2026-05-29 17:06:39 +00:00
package app
import (
"context"
2026-05-29 19:57:08 -05:00
"encoding/json"
2026-05-29 17:32:57 +00:00
"errors"
2026-05-29 17:06:39 +00:00
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
2026-05-29 17:17:50 +00:00
"time"
2026-05-29 17:06:39 +00:00
2026-05-29 17:32:57 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
2026-05-29 17:53:03 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
2026-05-29 17:06:39 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
2026-06-09 20:51:22 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
2026-06-09 21:08:17 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
2026-05-29 17:17:50 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
2026-05-29 17:40:38 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/state"
2026-06-09 21:20:53 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
2026-05-29 17:06:39 +00:00
)
func TestFetchAndSaveBundle ( t * testing . T ) {
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" :
_ , _ = w . Write ( [ ] byte ( ` { "data": { "issuedAt":"2026-05-29T10:30:00-05:00","product":"hourly","periods":[ { "startTime":"2026-05-29T13:00:00-05:00","endTime":"2026-05-29T14:00:00-05:00"}]}} ` ) )
case "/forecast/narrative" :
_ , _ = w . Write ( [ ] byte ( ` { "data": { "issuedAt":"2026-05-29T10:30:00-05:00","product":"narrative","periods":[]}} ` ) )
case "/alerts/active" :
_ , _ = w . Write ( [ ] byte ( ` { "data": { "alerts":[]}} ` ) )
case "/discussion" :
2026-05-29 23:29:04 -05:00
_ , _ = w . Write ( [ ] byte ( ` { "data": { "product":"discussion","issuedAt":"2026-05-29T09:25:00-05:00","keyMessages":[],"shortTerm": { "qualifier":"(Short Term)","text":"Short-term AFD narrative for saved bundle."},"longTerm": { "qualifier":"(Long Term)","text":"Long-term AFD narrative for saved bundle."}}} ` ) )
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-05-29 17:06:39 +00:00
default :
http . NotFound ( w , r )
}
} ) )
defer server . Close ( )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
path := filepath . Join ( t . TempDir ( ) , "bundle.json" )
bundle , err := FetchAndSaveBundle ( context . Background ( ) , FetchBundleRequest { Config : cfg , OutputPath : path } )
if err != nil {
t . Fatalf ( "FetchAndSaveBundle() error = %v" , err )
}
if bundle . Hourly == nil {
t . Fatal ( "Hourly = nil, want fetched bundle" )
}
data , err := os . ReadFile ( path )
if err != nil {
t . Fatalf ( "read saved bundle: %v" , err )
}
if ! strings . Contains ( string ( data ) , ` "product": "hourly" ` ) {
t . Fatalf ( "saved bundle missing hourly product:\n%s" , string ( data ) )
}
2026-05-30 07:47:49 -05:00
if ! strings . Contains ( string ( data ) , ` "title": "Several Chances for Rain Through Monday" ` ) {
t . Fatalf ( "saved bundle missing weather story title:\n%s" , string ( data ) )
}
2026-05-29 17:06:39 +00:00
}
func TestFetchAndSaveBundleRequiresOutputPath ( t * testing . T ) {
_ , err := FetchAndSaveBundle ( context . Background ( ) , FetchBundleRequest { Config : config . Defaults ( ) } )
if err == nil {
t . Fatal ( "FetchAndSaveBundle() error = nil, want output path error" )
}
if ! strings . Contains ( err . Error ( ) , "output path" ) {
t . Fatalf ( "error = %q, want output path context" , err . Error ( ) )
}
}
2026-05-29 17:17:50 +00:00
2026-05-29 20:44:25 +00:00
func TestGenerateReportWritesReportAndPreflight ( t * testing . T ) {
2026-05-29 17:32:57 +00:00
server := dailyBundleServer ( t )
2026-05-29 20:44:25 +00:00
cfg := dailyTestConfig ( t , server )
2026-05-29 17:32:57 +00:00
cfg . Workspace . Root = t . TempDir ( )
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
renderer := & recordingRenderer {
2026-05-29 17:47:42 +00:00
renderResult : & scriptorium . RenderResult {
2026-05-29 17:32:57 +00:00
Command : [ ] string { "scriptorium" , "render" } ,
Stdout : ` { "prepared":true} ` ,
ExitCode : 0 ,
} ,
2026-05-29 17:47:42 +00:00
runResult : & scriptorium . RunResult {
Command : [ ] string { "scriptorium" , "run" } ,
Stderr : "wrote report" ,
ExitCode : 0 ,
OutputPath : "" ,
} ,
runBody : "# Daily Report\n\nRain this morning.\n" ,
2026-05-29 17:32:57 +00:00
}
2026-05-29 17:47:42 +00:00
outputPath := filepath . Join ( t . TempDir ( ) , "daily.md" )
2026-06-09 21:12:13 +00:00
filesystemStore , err := state . NewFilesystemStore ( cfg . Workspace )
if err != nil {
t . Fatalf ( "NewFilesystemStore() error = %v" , err )
}
store := & recordingStore { Store : filesystemStore }
2026-05-29 17:32:57 +00:00
2026-05-29 20:44:25 +00:00
result , err := GenerateReport ( context . Background ( ) , ReportRequest {
2026-05-29 17:47:42 +00:00
Config : cfg ,
Resolved : resolved ,
OutputPath : outputPath ,
Renderer : renderer ,
2026-06-09 21:12:13 +00:00
Store : store ,
2026-05-29 17:32:57 +00:00
} )
if err != nil {
2026-05-29 20:44:25 +00:00
t . Fatalf ( "GenerateReport() error = %v" , err )
2026-05-29 17:32:57 +00:00
}
2026-05-29 17:47:42 +00:00
if renderer . renderCalls != 1 {
t . Fatalf ( "render calls = %d, want 1" , renderer . renderCalls )
}
if renderer . runCalls != 1 {
t . Fatalf ( "run calls = %d, want 1" , renderer . runCalls )
2026-05-29 17:32:57 +00:00
}
2026-05-29 17:47:42 +00:00
if renderer . renderRequest . PromptID != "weather.daily_report" {
t . Fatalf ( "render PromptID = %q, want weather.daily_report" , renderer . renderRequest . PromptID )
2026-05-29 17:32:57 +00:00
}
2026-05-29 17:47:42 +00:00
if renderer . renderRequest . DataPackagePath != result . DataPackagePath {
t . Fatalf ( "render DataPackagePath = %q, want managed path %q" , renderer . renderRequest . DataPackagePath , result . DataPackagePath )
2026-05-29 17:32:57 +00:00
}
2026-05-29 17:47:42 +00:00
if renderer . runRequest . DataPackagePath != result . DataPackagePath {
t . Fatalf ( "run DataPackagePath = %q, want managed path %q" , renderer . runRequest . DataPackagePath , result . DataPackagePath )
}
if renderer . runRequest . OutputPath != result . ReportPath {
t . Fatalf ( "run OutputPath = %q, want managed report path %q" , renderer . runRequest . OutputPath , result . ReportPath )
}
2026-06-09 21:33:34 +00:00
if got , want := strings . Join ( store . calls , "," ) , "module_snapshot,data_package,preflight,metadata,prepare_report,metadata" ; ! strings . HasPrefix ( got , want ) {
2026-06-09 21:12:13 +00:00
t . Fatalf ( "store calls = %v, want prefix %s" , store . calls , want )
}
2026-06-09 21:33:34 +00:00
assertPathsExist ( t , result . ModuleSnapshotPath , result . DataPackagePath , result . PreflightPath , result . ReportPath , result . MetadataPath , outputPath )
2026-06-09 20:58:35 +00:00
snapshotData , err := os . ReadFile ( result . ModuleSnapshotPath )
if err != nil {
t . Fatalf ( "read module snapshot: %v" , err )
}
if ! strings . Contains ( string ( snapshotData ) , module . SnapshotSchemaVersion ) || ! strings . Contains ( string ( snapshotData ) , ` "metadata" ` ) || ! strings . Contains ( string ( snapshotData ) , ` "derived_daily_summary" ` ) {
t . Fatalf ( "module snapshot missing expected stanzas:\n%s" , string ( snapshotData ) )
}
2026-05-29 17:47:42 +00:00
data , err := os . ReadFile ( result . DataPackagePath )
2026-05-29 17:32:57 +00:00
if err != nil {
t . Fatalf ( "read data package: %v" , err )
}
2026-06-09 21:08:17 +00:00
if ! strings . HasSuffix ( result . DataPackagePath , ".data_package.yaml" ) {
t . Fatalf ( "DataPackagePath = %q, want YAML data package path" , result . DataPackagePath )
2026-05-29 17:32:57 +00:00
}
2026-06-09 21:08:17 +00:00
if ! strings . Contains ( string ( data ) , "schema_version: weatherreporter.data_package.v2" ) ||
! strings . Contains ( string ( data ) , "recent_changes:" ) ||
! strings . Contains ( string ( data ) , "current_conditions:" ) ||
! strings . Contains ( string ( data ) , "area_forecast_discussion:" ) {
t . Fatalf ( "data package missing expected content:\n%s" , string ( data ) )
2026-05-29 19:57:08 -05:00
}
2026-06-09 21:08:17 +00:00
savedDataPackage , err := promptinput . LoadYAML ( data )
if err != nil {
2026-05-29 19:57:08 -05:00
t . Fatalf ( "decode data package: %v" , err )
}
2026-05-29 23:29:04 -05:00
if savedDataPackage . Report . CurrentLocalDate != "2026-05-29" {
t . Fatalf ( "data package currentLocalDate = %q, want 2026-05-29" , savedDataPackage . Report . CurrentLocalDate )
}
2026-06-09 21:08:17 +00:00
if _ , ok := savedDataPackage . Briefing . Values [ "metadata" ] ; ! ok {
t . Fatal ( "data package metadata stanza missing" )
2026-05-29 19:57:08 -05:00
}
2026-06-09 21:08:17 +00:00
current , ok := savedDataPackage . Briefing . Values [ "current_conditions" ] . ( map [ string ] any )
if ! ok || current [ "condition_text" ] != "Clear" {
t . Fatalf ( "data package current conditions = %#v, want current conditions" , savedDataPackage . Briefing . Values [ "current_conditions" ] )
2026-05-29 20:04:27 -05:00
}
2026-06-09 21:08:17 +00:00
story , ok := savedDataPackage . Briefing . Values [ "weather_story" ] . ( map [ string ] any )
if ! ok || story [ "title" ] != "Several Chances for Rain Through Monday" {
t . Fatalf ( "data package weather story = %#v, want weather story title" , savedDataPackage . Briefing . Values [ "weather_story" ] )
2026-05-30 07:47:49 -05:00
}
2026-05-29 19:42:07 -05:00
if ! strings . Contains ( string ( data ) , "Short-term AFD narrative for generated report." ) || ! strings . Contains ( string ( data ) , "Long-term AFD narrative for generated report." ) {
t . Fatalf ( "data package missing AFD short/long-term discussion:\n%s" , string ( data ) )
}
2026-05-29 17:32:57 +00:00
preflight , err := os . ReadFile ( result . PreflightPath )
if err != nil {
t . Fatalf ( "read preflight: %v" , err )
}
if ! strings . Contains ( string ( preflight ) , ` prepared ` ) {
t . Fatalf ( "preflight output missing render stdout:\n%s" , string ( preflight ) )
}
2026-05-29 17:40:38 +00:00
if result . Metadata . RunID != resolved . Metadata ( ) . RunID {
t . Fatalf ( "metadata RunID = %q, want %q" , result . Metadata . RunID , resolved . Metadata ( ) . RunID )
}
2026-06-09 21:33:34 +00:00
if result . Metadata . ModuleSnapshotPath != result . ModuleSnapshotPath || result . Metadata . DataPackagePath != result . DataPackagePath {
2026-05-29 17:40:38 +00:00
t . Fatalf ( "metadata does not link artifact paths: %#v" , result . Metadata )
}
2026-05-29 17:47:42 +00:00
if result . Metadata . RenderedReportPath != result . ReportPath {
t . Fatalf ( "metadata rendered report path = %q, want %q" , result . Metadata . RenderedReportPath , result . ReportPath )
}
2026-05-29 17:53:03 +00:00
if len ( result . RecentChanges ) != 0 {
t . Fatalf ( "RecentChanges = %#v, want none without prior snapshot" , result . RecentChanges )
}
2026-05-29 17:47:42 +00:00
report , err := os . ReadFile ( outputPath )
if err != nil {
t . Fatalf ( "read report output: %v" , err )
}
if ! strings . Contains ( string ( report ) , "# Daily Report" ) {
t . Fatalf ( "report output missing markdown:\n%s" , string ( report ) )
}
2026-05-29 17:32:57 +00:00
}
2026-06-07 23:38:03 +00:00
func TestGenerateReportDisabledNotificationDoesNotCallNotifier ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := dailyTestConfig ( t , server )
cfg . Workspace . Root = t . TempDir ( )
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
notifier := & recordingNotifier { }
_ , err = GenerateReport ( context . Background ( ) , ReportRequest {
Config : cfg ,
Resolved : resolved ,
Renderer : successfulRenderer ( "# Daily Report\n" ) ,
Notifier : notifier ,
} )
if err != nil {
t . Fatalf ( "GenerateReport() error = %v" , err )
}
if len ( notifier . requests ) != 0 {
t . Fatalf ( "notification requests = %#v, want none when disabled" , notifier . requests )
}
}
func TestGenerateReportNotifiesManagedReportPath ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := dailyTestConfig ( t , server )
cfg . Workspace . Root = t . TempDir ( )
cfg . Notify . Distributor . Enabled = true
2026-06-08 07:04:31 -05:00
cfg . Notify . Distributor . PipelineIDTemplate = "weatherreporter.{artifact_group}"
2026-06-07 23:38:03 +00:00
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
notifier := & recordingNotifier {
2026-06-07 20:21:26 -05:00
result : & NotificationResult {
RunID : "distributor-run" ,
Status : "succeeded" ,
UploadStatus : "accepted" ,
PipelineID : "reports" ,
Report : [ ] byte ( ` { "actions":[ { "action":"replace_older"}]} ` ) ,
} ,
2026-06-07 23:38:03 +00:00
}
outputPath := filepath . Join ( t . TempDir ( ) , "daily-copy.md" )
result , err := GenerateReport ( context . Background ( ) , ReportRequest {
Config : cfg ,
Resolved : resolved ,
OutputPath : outputPath ,
Renderer : successfulRenderer ( "# Daily Report\n" ) ,
Notifier : notifier ,
} )
if err != nil {
t . Fatalf ( "GenerateReport() error = %v" , err )
}
if result . Notification == nil {
t . Fatal ( "Notification = nil, want notification result" )
}
2026-06-07 20:21:26 -05:00
if result . Notification . RunID != "distributor-run" || result . Notification . Status != "succeeded" {
t . Fatalf ( "Notification = %#v, want succeeded distributor run" , result . Notification )
}
if result . NotificationPath == "" || result . Metadata . NotificationPath != result . NotificationPath {
t . Fatalf ( "NotificationPath result=%q metadata=%q, want linked artifact" , result . NotificationPath , result . Metadata . NotificationPath )
}
notificationData , err := os . ReadFile ( result . NotificationPath )
if err != nil {
t . Fatalf ( "read notification artifact: %v" , err )
}
2026-06-08 07:04:31 -05:00
var notificationArtifact state . DistributorNotificationArtifact
if err := json . Unmarshal ( notificationData , & notificationArtifact ) ; err != nil {
t . Fatalf ( "decode notification artifact: %v" , err )
}
2026-06-08 10:29:42 -05:00
wantBundlePaths := [ ] string {
"2026-05-29/daily/2026-05-29-daily-" + result . Metadata . RunID + ".md" ,
}
if notificationArtifact . PipelineID != "weatherreporter.daily" || strings . Join ( notificationArtifact . BundlePaths , "\n" ) != strings . Join ( wantBundlePaths , "\n" ) || notificationArtifact . BundleCreated . IsZero ( ) || notificationArtifact . RunStatus == nil || ! strings . Contains ( string ( notificationArtifact . RunStatus . Report ) , "replace_older" ) {
2026-06-08 07:04:31 -05:00
t . Fatalf ( "notification artifact = %#v, want requested pipeline, status report, and created timestamp" , notificationArtifact )
2026-06-07 23:38:03 +00:00
}
if len ( notifier . requests ) != 1 {
t . Fatalf ( "notification requests = %d, want 1" , len ( notifier . requests ) )
}
req := notifier . requests [ 0 ]
if req . ReportPath != result . ReportPath {
t . Fatalf ( "notification ReportPath = %q, want managed path %q" , req . ReportPath , result . ReportPath )
}
if req . ReportPath == outputPath {
t . Fatalf ( "notification used output copy %q, want managed report path" , outputPath )
}
2026-06-08 10:29:42 -05:00
if strings . Join ( req . BundlePaths , "\n" ) != strings . Join ( wantBundlePaths , "\n" ) {
t . Fatalf ( "notification BundlePaths = %#v, want %#v" , req . BundlePaths , wantBundlePaths )
2026-06-07 23:38:03 +00:00
}
2026-06-08 07:04:31 -05:00
if req . PipelineID != "weatherreporter.daily" {
t . Fatalf ( "notification PipelineID = %q, want rendered pipeline" , req . PipelineID )
}
if req . BundleID != "weatherreporter.home.daily_today" {
2026-06-07 23:38:03 +00:00
t . Fatalf ( "notification BundleID = %q, want default template" , req . BundleID )
}
2026-06-08 07:04:31 -05:00
if req . IdempotencyKey != req . BundleID + "." + result . Metadata . RunID {
t . Fatalf ( "IdempotencyKey = %q, want per-run key" , req . IdempotencyKey )
2026-06-07 23:38:03 +00:00
}
if req . RunID != result . Metadata . RunID {
t . Fatalf ( "notification RunID = %q, want report run id %q" , req . RunID , result . Metadata . RunID )
}
2026-06-07 20:21:26 -05:00
if ! req . CreatedAt . Equal ( result . Metadata . GeneratedAt ) {
t . Fatalf ( "notification CreatedAt = %s, want generated at %s" , req . CreatedAt , result . Metadata . GeneratedAt )
}
2026-06-07 23:38:03 +00:00
}
func TestGenerateReportNotificationFailureFailsReport ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := dailyTestConfig ( t , server )
cfg . Workspace . Root = t . TempDir ( )
cfg . Notify . Distributor . Enabled = true
2026-06-08 07:04:31 -05:00
cfg . Notify . Distributor . PipelineIDTemplate = "weatherreporter.{artifact_group}"
2026-06-07 23:38:03 +00:00
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
notifier := & recordingNotifier { err : errors . New ( "upload rejected" ) }
2026-06-07 20:21:26 -05:00
store , err := state . NewFilesystemStore ( cfg . Workspace )
if err != nil {
t . Fatalf ( "NewFilesystemStore() error = %v" , err )
}
2026-06-07 23:38:03 +00:00
_ , err = GenerateReport ( context . Background ( ) , ReportRequest {
Config : cfg ,
Resolved : resolved ,
Renderer : successfulRenderer ( "# Daily Report\n" ) ,
2026-06-07 20:21:26 -05:00
Store : store ,
2026-06-07 23:38:03 +00:00
Notifier : notifier ,
} )
if err == nil {
t . Fatal ( "GenerateReport() error = nil, want notification error" )
}
if ! strings . Contains ( err . Error ( ) , "notify report" ) || ! strings . Contains ( err . Error ( ) , "upload rejected" ) {
t . Fatalf ( "error = %q, want notification context" , err . Error ( ) )
}
if len ( notifier . requests ) != 1 {
t . Fatalf ( "notification requests = %d, want one attempted notification" , len ( notifier . requests ) )
}
2026-06-07 20:21:26 -05:00
paths , pathErr := store . Paths ( resolved )
if pathErr != nil {
t . Fatalf ( "Paths() error = %v" , pathErr )
}
notificationData , readErr := os . ReadFile ( paths . Notification )
if readErr != nil {
t . Fatalf ( "read notification artifact after failure: %v" , readErr )
}
var notification state . DistributorNotificationArtifact
if err := json . Unmarshal ( notificationData , & notification ) ; err != nil {
t . Fatalf ( "decode notification artifact: %v" , err )
}
if notification . Status != "failed" || ! strings . Contains ( notification . Error , "upload rejected" ) {
t . Fatalf ( "notification failure artifact = %+v, want failed upload context" , notification )
}
2026-06-07 23:38:03 +00:00
}
func TestGenerateReportDoesNotNotifyAfterRenderOrRunFailure ( t * testing . T ) {
server := dailyBundleServer ( t )
tests := [ ] struct {
name string
renderer Renderer
} {
{
name : "Render" ,
renderer : & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 1 , Stderr : "render failed" } ,
err : errors . New ( "render failed" ) ,
} ,
} ,
{
name : "Run" ,
renderer : & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult { ExitCode : 2 , Stderr : "run failed" } ,
runErr : errors . New ( "run failed" ) ,
runBody : "# Daily Report\n" ,
} ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
cfg := dailyTestConfig ( t , server )
cfg . Workspace . Root = t . TempDir ( )
cfg . Notify . Distributor . Enabled = true
2026-06-08 07:04:31 -05:00
cfg . Notify . Distributor . PipelineIDTemplate = "weatherreporter.{artifact_group}"
2026-06-07 23:38:03 +00:00
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
notifier := & recordingNotifier { }
_ , err = GenerateReport ( context . Background ( ) , ReportRequest {
Config : cfg ,
Resolved : resolved ,
Renderer : tt . renderer ,
Notifier : notifier ,
} )
if err == nil {
t . Fatal ( "GenerateReport() error = nil, want generation error" )
}
if len ( notifier . requests ) != 0 {
t . Fatalf ( "notification requests = %#v, want none after generation failure" , notifier . requests )
}
} )
}
}
func TestGenerateReportDoesNotNotifyAfterFetchFailure ( t * testing . T ) {
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = ""
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
cfg . Notify . Distributor . Enabled = true
2026-06-08 07:04:31 -05:00
cfg . Notify . Distributor . PipelineIDTemplate = "weatherreporter.{artifact_group}"
2026-06-07 23:38:03 +00:00
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
notifier := & recordingNotifier { }
_ , err = GenerateReport ( context . Background ( ) , ReportRequest {
Config : cfg ,
Resolved : resolved ,
Renderer : successfulRenderer ( "# Daily Report\n" ) ,
Notifier : notifier ,
} )
if err == nil {
t . Fatal ( "GenerateReport() error = nil, want fetch setup error" )
}
if len ( notifier . requests ) != 0 {
t . Fatalf ( "notification requests = %#v, want none after fetch failure" , notifier . requests )
}
}
2026-05-29 20:44:25 +00:00
func TestGenerateReportPersistsFailedPreflight ( t * testing . T ) {
2026-05-29 17:32:57 +00:00
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
renderer := & recordingRenderer {
2026-05-29 17:47:42 +00:00
renderResult : & scriptorium . RenderResult {
2026-05-29 17:32:57 +00:00
Command : [ ] string { "scriptorium" , "render" } ,
Stderr : "render failed" ,
ExitCode : 1 ,
} ,
err : errors . New ( "scriptorium render exited with code 1: render failed" ) ,
}
2026-05-29 20:44:25 +00:00
_ , err = GenerateReport ( context . Background ( ) , ReportRequest {
2026-05-29 17:32:57 +00:00
Config : cfg ,
Resolved : resolved ,
Renderer : renderer ,
} )
if err == nil {
2026-05-29 20:44:25 +00:00
t . Fatal ( "GenerateReport() error = nil, want render error" )
2026-05-29 17:32:57 +00:00
}
2026-05-29 17:40:38 +00:00
store , err := state . NewFilesystemStore ( cfg . Workspace )
if err != nil {
t . Fatalf ( "NewFilesystemStore() error = %v" , err )
}
paths , err := store . Paths ( resolved )
if err != nil {
t . Fatalf ( "Paths() error = %v" , err )
}
preflightPath := paths . Preflight
2026-05-29 17:32:57 +00:00
preflight , readErr := os . ReadFile ( preflightPath )
if readErr != nil {
t . Fatalf ( "read failed preflight: %v" , readErr )
}
if ! strings . Contains ( string ( preflight ) , ` "exitCode": 1 ` ) {
t . Fatalf ( "failed preflight was not persisted:\n%s" , string ( preflight ) )
}
2026-05-29 17:40:38 +00:00
if _ , err := os . Stat ( paths . Metadata ) ; err != nil {
t . Fatalf ( "expected metadata for failed preflight %q: %v" , paths . Metadata , err )
}
2026-05-29 17:47:42 +00:00
if renderer . runCalls != 0 {
t . Fatalf ( "run calls = %d, want none after failed preflight" , renderer . runCalls )
}
}
2026-05-29 20:44:25 +00:00
func TestGenerateReportReturnsRunErrorAfterPreflight ( t * testing . T ) {
2026-05-29 17:47:42 +00:00
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
renderer := & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult {
Stderr : "validation failed" ,
ExitCode : 2 ,
} ,
runErr : errors . New ( "scriptorium run exited with code 2: validation failed" ) ,
runBody : "# Daily Report\n" ,
}
2026-05-29 20:44:25 +00:00
_ , err = GenerateReport ( context . Background ( ) , ReportRequest {
2026-05-29 17:47:42 +00:00
Config : cfg ,
Resolved : resolved ,
Renderer : renderer ,
} )
if err == nil {
2026-05-29 20:44:25 +00:00
t . Fatal ( "GenerateReport() error = nil, want run error" )
2026-05-29 17:47:42 +00:00
}
if renderer . renderCalls != 1 || renderer . runCalls != 1 {
t . Fatalf ( "calls render=%d run=%d, want one of each" , renderer . renderCalls , renderer . runCalls )
}
store , err := state . NewFilesystemStore ( cfg . Workspace )
if err != nil {
t . Fatalf ( "NewFilesystemStore() error = %v" , err )
}
paths , err := store . Paths ( resolved )
if err != nil {
t . Fatalf ( "Paths() error = %v" , err )
}
if _ , err := os . Stat ( paths . Metadata ) ; err != nil {
t . Fatalf ( "expected metadata for failed run %q: %v" , paths . Metadata , err )
}
if _ , err := os . Stat ( paths . RenderedReport ) ; err != nil {
t . Fatalf ( "expected report from validation exit %q: %v" , paths . RenderedReport , err )
}
2026-05-29 17:32:57 +00:00
}
2026-05-29 20:44:25 +00:00
func TestGenerateReportIncludesRecentChangesFromPriorSnapshot ( t * testing . T ) {
2026-05-29 17:53:03 +00:00
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
store , err := state . NewFilesystemStore ( cfg . Workspace )
if err != nil {
t . Fatalf ( "NewFilesystemStore() error = %v" , err )
}
priorResolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T04:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate(prior) error = %v" , err )
}
2026-06-09 21:33:34 +00:00
savePriorRun ( t , store , priorResolved , priorDailyModuleSnapshot ( t , priorResolved ) )
2026-05-29 17:53:03 +00:00
currentResolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate(current) error = %v" , err )
}
renderer := & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult { ExitCode : 0 } ,
runBody : "# Daily Report\n" ,
}
2026-05-29 20:44:25 +00:00
result , err := GenerateReport ( context . Background ( ) , ReportRequest {
2026-05-29 17:53:03 +00:00
Config : cfg ,
Resolved : currentResolved ,
Renderer : renderer ,
Store : store ,
} )
if err != nil {
2026-05-29 20:44:25 +00:00
t . Fatalf ( "GenerateReport() error = %v" , err )
2026-05-29 17:53:03 +00:00
}
if len ( result . RecentChanges ) == 0 {
t . Fatal ( "RecentChanges length = 0, want changes from prior snapshot" )
}
data , err := os . ReadFile ( result . DataPackagePath )
if err != nil {
t . Fatalf ( "read data package: %v" , err )
}
if ! strings . Contains ( string ( data ) , "alert_added" ) || ! strings . Contains ( string ( data ) , "temperature_shift" ) {
t . Fatalf ( "data package missing recent changes:\n%s" , string ( data ) )
}
}
2026-05-29 18:00:27 +00:00
func TestGenerateTomorrowReportUsesTomorrowBriefingDate ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportTomorrow ,
} , mustParse ( "2026-05-29T18:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
renderer := & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult { ExitCode : 0 } ,
runBody : "# Tomorrow Planning Brief\n" ,
}
2026-05-29 20:44:25 +00:00
result , err := GenerateReport ( context . Background ( ) , ReportRequest {
2026-05-29 18:00:27 +00:00
Config : cfg ,
Resolved : resolved ,
Renderer : renderer ,
} )
if err != nil {
2026-05-29 20:44:25 +00:00
t . Fatalf ( "GenerateReport() error = %v" , err )
2026-05-29 18:00:27 +00:00
}
2026-06-09 21:33:34 +00:00
if result . Metadata . ReportID != report . DailyTomorrow || result . Metadata . Variant != "tomorrow" {
t . Fatalf ( "metadata report/variant = %q/%q, want tomorrow" , result . Metadata . ReportID , result . Metadata . Variant )
2026-05-29 18:00:27 +00:00
}
2026-06-09 21:33:34 +00:00
dailySummary , ok , err := module . StanzaValue [ map [ string ] any ] ( result . ModuleSnapshot , "derived_daily_summary" )
if err != nil {
t . Fatalf ( "decode daily summary: %v" , err )
}
if ! ok || dailySummary [ "date" ] != "2026-05-30" {
t . Fatalf ( "daily summary = %#v, want tomorrow date" , dailySummary )
2026-05-29 18:00:27 +00:00
}
2026-06-09 21:33:34 +00:00
if _ , ok := result . ModuleSnapshot . LookupStanza ( "tomorrow_planning" ) ; ! ok {
t . Fatal ( "tomorrow_planning stanza missing" )
2026-05-29 18:00:27 +00:00
}
if ! strings . Contains ( filepath . Base ( result . ReportPath ) , "daily_tomorrow" ) {
t . Fatalf ( "ReportPath = %q, want managed tomorrow report path" , result . ReportPath )
}
}
func TestTomorrowReportCanCompareAgainstPriorDailySnapshot ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
store , err := state . NewFilesystemStore ( cfg . Workspace )
if err != nil {
t . Fatalf ( "NewFilesystemStore() error = %v" , err )
}
priorResolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-30T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T17:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate(prior) error = %v" , err )
}
2026-06-09 21:33:34 +00:00
savePriorRun ( t , store , priorResolved , priorDailyModuleSnapshot ( t , priorResolved ) )
2026-05-29 18:00:27 +00:00
currentResolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportTomorrow ,
} , mustParse ( "2026-05-29T18:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate(current) error = %v" , err )
}
renderer := & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult { ExitCode : 0 } ,
runBody : "# Tomorrow Planning Brief\n" ,
}
2026-05-29 20:44:25 +00:00
result , err := GenerateReport ( context . Background ( ) , ReportRequest {
2026-05-29 18:00:27 +00:00
Config : cfg ,
Resolved : currentResolved ,
Renderer : renderer ,
Store : store ,
} )
if err != nil {
2026-05-29 20:44:25 +00:00
t . Fatalf ( "GenerateReport() error = %v" , err )
2026-05-29 18:00:27 +00:00
}
if result . PriorSnapshot == nil {
t . Fatal ( "PriorSnapshot = nil, want compatible prior daily snapshot" )
}
if len ( result . RecentChanges ) == 0 {
t . Fatal ( "RecentChanges length = 0, want changes from compatible prior daily snapshot" )
}
}
2026-05-29 18:08:50 +00:00
func TestGenerateThreeDayReportWritesReportAndRecentChanges ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
store , err := state . NewFilesystemStore ( cfg . Workspace )
if err != nil {
t . Fatalf ( "NewFilesystemStore() error = %v" , err )
}
priorResolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportThreeDay ,
} , mustParse ( "2026-05-29T04:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate(prior) error = %v" , err )
}
2026-06-09 21:33:34 +00:00
savePriorRun ( t , store , priorResolved , priorOutlookModuleSnapshot ( t , "2026-05-30" ) )
2026-05-29 18:08:50 +00:00
currentResolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportThreeDay ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate(current) error = %v" , err )
}
renderer := & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult { ExitCode : 0 } ,
runBody : "# 3-Day Outlook\n" ,
}
result , err := GenerateReport ( context . Background ( ) , ReportRequest {
Config : cfg ,
Resolved : currentResolved ,
Renderer : renderer ,
Store : store ,
} )
if err != nil {
t . Fatalf ( "GenerateReport() error = %v" , err )
}
2026-06-09 21:33:34 +00:00
dayparts , ok , err := module . StanzaValue [ map [ string ] any ] ( result . ModuleSnapshot , "derived_daypart_summaries" )
if err != nil {
t . Fatalf ( "decode daypart summaries: %v" , err )
2026-05-29 18:08:50 +00:00
}
2026-06-09 21:33:34 +00:00
if ! ok || len ( dayparts ) == 0 {
t . Fatalf ( "daypart summaries = %#v, want 3-day module content" , dayparts )
2026-05-29 18:08:50 +00:00
}
if renderer . renderRequest . PromptID != "weather.three_day_outlook" {
t . Fatalf ( "render PromptID = %q, want weather.three_day_outlook" , renderer . renderRequest . PromptID )
}
if result . PriorSnapshot == nil {
t . Fatal ( "PriorSnapshot = nil, want prior 3-day snapshot" )
}
if len ( result . RecentChanges ) == 0 {
t . Fatal ( "RecentChanges length = 0, want changes from prior 3-day snapshot" )
}
}
2026-05-29 18:16:10 +00:00
func TestGenerateWeekendReportWritesReportAndRecentChanges ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
store , err := state . NewFilesystemStore ( cfg . Workspace )
if err != nil {
t . Fatalf ( "NewFilesystemStore() error = %v" , err )
}
priorResolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportWeekend ,
} , mustParse ( "2026-05-29T04:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate(prior) error = %v" , err )
}
2026-06-09 21:33:34 +00:00
savePriorRun ( t , store , priorResolved , priorOutlookModuleSnapshot ( t , "2026-05-30" ) )
2026-05-29 18:16:10 +00:00
currentResolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportWeekend ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate(current) error = %v" , err )
}
renderer := & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult { ExitCode : 0 } ,
runBody : "# Weekend Outlook\n" ,
}
result , err := GenerateReport ( context . Background ( ) , ReportRequest {
Config : cfg ,
Resolved : currentResolved ,
Renderer : renderer ,
Store : store ,
} )
if err != nil {
t . Fatalf ( "GenerateReport() error = %v" , err )
}
2026-06-09 21:33:34 +00:00
dayparts , ok , err := module . StanzaValue [ map [ string ] any ] ( result . ModuleSnapshot , "derived_daypart_summaries" )
if err != nil {
t . Fatalf ( "decode daypart summaries: %v" , err )
2026-05-29 18:16:10 +00:00
}
2026-06-09 21:33:34 +00:00
if ! ok || len ( dayparts ) == 0 {
t . Fatalf ( "daypart summaries = %#v, want weekend module content" , dayparts )
2026-05-29 18:16:10 +00:00
}
if renderer . renderRequest . PromptID != "weather.weekend_outlook" {
t . Fatalf ( "render PromptID = %q, want weather.weekend_outlook" , renderer . renderRequest . PromptID )
}
if result . PriorSnapshot == nil {
t . Fatal ( "PriorSnapshot = nil, want prior weekend snapshot" )
}
if len ( result . RecentChanges ) == 0 {
t . Fatal ( "RecentChanges length = 0, want changes from prior weekend snapshot" )
}
}
2026-05-29 18:28:25 +00:00
func TestGenerateStormReportWritesReport ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportStorm ,
StormStart : mustParse ( "2026-05-29T06:00:00-05:00" ) ,
StormEnd : mustParse ( "2026-05-29T10:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
renderer := & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult { ExitCode : 0 } ,
runBody : "# Storm Report\n" ,
}
outputPath := filepath . Join ( t . TempDir ( ) , "storm.md" )
result , err := GenerateReport ( context . Background ( ) , ReportRequest {
Config : cfg ,
Resolved : resolved ,
OutputPath : outputPath ,
Renderer : renderer ,
} )
if err != nil {
t . Fatalf ( "GenerateReport() error = %v" , err )
}
if renderer . renderRequest . PromptID != "weather.storm_report" {
t . Fatalf ( "render PromptID = %q, want weather.storm_report" , renderer . renderRequest . PromptID )
}
2026-06-09 21:33:34 +00:00
if _ , ok := result . ModuleSnapshot . LookupStanza ( "precip_timing" ) ; ! ok {
t . Fatal ( "precip_timing stanza missing" )
2026-05-29 18:28:25 +00:00
}
if _ , err := os . Stat ( outputPath ) ; err != nil {
t . Fatalf ( "expected requested report output %q: %v" , outputPath , err )
}
data , err := os . ReadFile ( result . DataPackagePath )
if err != nil {
t . Fatalf ( "read data package: %v" , err )
}
2026-06-09 21:08:17 +00:00
if ! strings . Contains ( string ( data ) , "id: storm" ) || ! strings . Contains ( string ( data ) , "prompt_id: weather.storm_report" ) || ! strings . Contains ( string ( data ) , "precip_timing:" ) {
2026-05-29 18:28:25 +00:00
t . Fatalf ( "data package missing storm content:\n%s" , string ( data ) )
}
}
2026-05-29 18:33:24 +00:00
func TestInspectGeneratedReportArtifacts ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
renderer := & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult { ExitCode : 0 } ,
runBody : "# Daily Report\n" ,
}
result , err := GenerateReport ( context . Background ( ) , ReportRequest {
Config : cfg ,
Resolved : resolved ,
Renderer : renderer ,
} )
if err != nil {
t . Fatalf ( "GenerateReport() error = %v" , err )
}
records , err := InspectReports ( context . Background ( ) , InspectReportsRequest { Config : cfg , Limit : 1 } )
if err != nil {
t . Fatalf ( "InspectReports() error = %v" , err )
}
if len ( records ) != 1 || records [ 0 ] . RunID != result . Metadata . RunID {
t . Fatalf ( "records = %#v, want generated run" , records )
}
metadata , err := InspectMetadata ( context . Background ( ) , InspectRunRequest { Config : cfg , RunID : result . Metadata . RunID } )
if err != nil {
t . Fatalf ( "InspectMetadata() error = %v" , err )
}
2026-06-09 21:33:34 +00:00
if metadata . ModuleSnapshotPath != result . ModuleSnapshotPath || metadata . DataPackagePath != result . DataPackagePath {
2026-05-29 18:33:24 +00:00
t . Fatalf ( "metadata paths = %#v, want generated artifact paths" , metadata )
}
2026-06-09 20:58:35 +00:00
moduleSnapshot , err := InspectModules ( context . Background ( ) , InspectRunRequest { Config : cfg , RunID : result . Metadata . RunID } )
2026-05-29 18:33:24 +00:00
if err != nil {
2026-06-09 20:58:35 +00:00
t . Fatalf ( "InspectModules() error = %v" , err )
2026-05-29 18:33:24 +00:00
}
2026-06-09 20:58:35 +00:00
if moduleSnapshot . SchemaVersion != module . SnapshotSchemaVersion || len ( moduleSnapshot . Outputs ) == 0 {
t . Fatalf ( "module snapshot = %#v, want persisted outputs" , moduleSnapshot )
2026-05-29 18:33:24 +00:00
}
dataPackage , err := InspectDataPackage ( context . Background ( ) , InspectRunRequest { Config : cfg , RunID : result . Metadata . RunID } )
if err != nil {
t . Fatalf ( "InspectDataPackage() error = %v" , err )
}
if dataPackage . RunID != result . Metadata . RunID {
t . Fatalf ( "data package RunID = %q, want %q" , dataPackage . RunID , result . Metadata . RunID )
}
sources , err := InspectSources ( context . Background ( ) , InspectRunRequest { Config : cfg , RunID : result . Metadata . RunID } )
if err != nil {
t . Fatalf ( "InspectSources() error = %v" , err )
}
if len ( sources . Sources ) == 0 || len ( sources . Warnings ) == 0 {
t . Fatalf ( "sources = %#v, want provenance and warnings" , sources )
}
}
func TestInspectPriorSnapshot ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
store , err := state . NewFilesystemStore ( cfg . Workspace )
if err != nil {
t . Fatalf ( "NewFilesystemStore() error = %v" , err )
}
priorResolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T04:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate(prior) error = %v" , err )
}
currentResolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportDaily ,
Date : mustParse ( "2026-05-29T12:00:00-05:00" ) ,
} , mustParse ( "2026-05-29T05:00:00-05:00" ) )
if err != nil {
t . Fatalf ( "ResolveGenerate(current) error = %v" , err )
}
renderer := & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult { ExitCode : 0 } ,
runBody : "# Daily Report\n" ,
}
if _ , err := GenerateReport ( context . Background ( ) , ReportRequest { Config : cfg , Resolved : priorResolved , Renderer : renderer , Store : store } ) ; err != nil {
t . Fatalf ( "GenerateReport(prior) error = %v" , err )
}
current , err := GenerateReport ( context . Background ( ) , ReportRequest { Config : cfg , Resolved : currentResolved , Renderer : renderer , Store : store } )
if err != nil {
t . Fatalf ( "GenerateReport(current) error = %v" , err )
}
prior , err := InspectPriorSnapshot ( context . Background ( ) , InspectRunRequest { Config : cfg , RunID : current . Metadata . RunID } )
if err != nil {
t . Fatalf ( "InspectPriorSnapshot() error = %v" , err )
}
if prior == nil || prior . Metadata . RunID != priorResolved . Metadata ( ) . RunID {
t . Fatalf ( "prior = %#v, want previous generated run" , prior )
}
}
func TestInspectMissingMetadata ( t * testing . T ) {
cfg := config . Defaults ( )
cfg . Workspace . Root = t . TempDir ( )
_ , err := InspectMetadata ( context . Background ( ) , InspectRunRequest { Config : cfg , RunID : "missing" } )
if err == nil {
t . Fatal ( "InspectMetadata() 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 17:17:50 +00:00
func TestResolveGenerateMapsCommandToReportDefinition ( t * testing . T ) {
cfg := config . Defaults ( )
cfg . WeatherAPI . Timezone = "America/Chicago"
now := mustParse ( "2026-05-29T18:00:00-05:00" )
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportTomorrow ,
} , now )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
if resolved . Definition . ID != report . DailyTomorrow {
t . Fatalf ( "ID = %q, want daily_tomorrow" , resolved . Definition . ID )
}
if resolved . Definition . PromptID != "weather.daily_report" {
t . Fatalf ( "PromptID = %q, want weather.daily_report" , resolved . Definition . PromptID )
}
if got := resolved . ValidPeriod . Start . Format ( "2006-01-02" ) ; got != "2026-05-30" {
t . Fatalf ( "valid start date = %s, want 2026-05-30" , got )
}
}
2026-06-09 20:51:22 +00:00
func TestResolveGenerateUsesConfiguredReportModules ( t * testing . T ) {
path := filepath . Join ( t . TempDir ( ) , "config.yml" )
if err := os . WriteFile ( path , [ ] byte ( `
reports :
tomorrow :
deterministic_modules :
- metadata
- alert_digest
- tomorrow_planning
` ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write config fixture: %v" , err )
}
cfg , err := config . LoadFile ( path )
if err != nil {
t . Fatalf ( "LoadFile() error = %v" , err )
}
now := mustParse ( "2026-05-29T18:00:00-05:00" )
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportTomorrow ,
} , now )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
want := [ ] module . ID { module . Metadata , module . AlertDigest , module . TomorrowPlanning }
if got := resolved . Definition . ModuleIDs ( ) ; strings . Join ( moduleIDsForTest ( got ) , "," ) != strings . Join ( moduleIDsForTest ( want ) , "," ) {
t . Fatalf ( "ModuleIDs() = %#v, want %#v" , got , want )
}
}
func moduleIDsForTest ( ids [ ] module . ID ) [ ] string {
out := make ( [ ] string , 0 , len ( ids ) )
for _ , id := range ids {
out = append ( out , string ( id ) )
}
return out
}
2026-05-29 17:23:49 +00:00
func dailyBundleServer ( 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" :
2026-05-29 20:04:27 -05:00
_ , _ = w . Write ( [ ] byte ( ` { "data": { "conditionText":"Clear","temperatureF":75,"relativeHumidityPercent":56,"windSpeedMph":8}} ` ) )
2026-05-29 17:23:49 +00:00
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":[ { "event":"Flood Watch","effective":"2026-05-29T05:00:00-05:00","expires":"2026-05-29T09:00:00-05:00"}]}} ` ) )
case "/discussion" :
2026-05-29 23:29:04 -05:00
_ , _ = w . Write ( [ ] byte ( ` { "data": { "product":"discussion","issuedAt":"2026-05-29T09:25:00-05:00","keyMessages":["Storms are most likely during the morning."],"shortTerm": { "qualifier":"(Short Term)","text":"Short-term AFD narrative for generated report."},"longTerm": { "qualifier":"(Long Term)","text":"Long-term AFD narrative for generated report."}}} ` ) )
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-05-29 17:23:49 +00:00
default :
http . NotFound ( w , r )
}
} ) )
t . Cleanup ( server . Close )
return server
}
2026-05-29 17:17:50 +00:00
func TestResolveGenerateStorm ( t * testing . T ) {
cfg := config . Defaults ( )
cfg . WeatherAPI . Timezone = "America/Chicago"
now := mustParse ( "2026-05-29T12:00:00-05:00" )
start := mustParse ( "2026-05-29T18:00:00-05:00" )
end := mustParse ( "2026-05-30T06:00:00-05:00" )
resolved , err := ResolveGenerate ( GenerateRequest {
Config : cfg ,
Report : ReportStorm ,
StormStart : start ,
StormEnd : end ,
} , now )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
if resolved . Definition . ID != report . Storm {
t . Fatalf ( "ID = %q, want storm" , resolved . Definition . ID )
}
if ! resolved . ValidPeriod . Start . Equal ( start ) || ! resolved . ValidPeriod . End . Equal ( end ) {
t . Fatalf ( "period = %#v, want storm window" , resolved . ValidPeriod )
}
}
func TestResolveBatchMorningSkipsWeekendOnSunday ( t * testing . T ) {
cfg := config . Defaults ( )
cfg . WeatherAPI . Timezone = "America/Chicago"
now := mustParse ( "2026-05-31T06:00:00-05:00" )
resolved , err := ResolveBatch ( BatchRequest { Config : cfg , Batch : BatchMorning } , now )
if err != nil {
t . Fatalf ( "ResolveBatch() error = %v" , err )
}
if len ( resolved ) != 2 {
t . Fatalf ( "resolved length = %d, want 2" , len ( resolved ) )
}
for _ , item := range resolved {
if item . Definition . ID == report . Weekend {
t . Fatal ( "morning batch included weekend on Sunday" )
}
}
}
2026-05-29 18:20:39 +00:00
func TestRunBatchContinuesAfterReportFailure ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
renderer := & selectiveRenderer {
failRenderPrompt : "weather.three_day_outlook" ,
runBody : "# Batch Report\n" ,
}
result , err := RunBatchDetailed ( context . Background ( ) , BatchRequest {
Config : cfg ,
Batch : BatchMorning ,
Now : mustParse ( "2026-05-29T05:00:00-05:00" ) ,
Renderer : renderer ,
} )
if err != nil {
t . Fatalf ( "RunBatchDetailed() error = %v" , err )
}
if result . Total != 3 || result . Succeeded != 2 || result . Failed != 1 {
t . Fatalf ( "summary total/succeeded/failed = %d/%d/%d, want 3/2/1" , result . Total , result . Succeeded , result . Failed )
}
if renderer . runCalls != 2 {
t . Fatalf ( "run calls = %d, want successful reports to continue" , renderer . runCalls )
}
var failedThreeDay bool
for _ , item := range result . Reports {
if item . ReportID == report . ThreeDay && item . Status == "failed" && strings . Contains ( item . Error , "render failed" ) {
failedThreeDay = true
}
if item . ReportID != report . ThreeDay && item . Status != "succeeded" {
t . Fatalf ( "report %s status = %s, want succeeded" , item . ReportID , item . Status )
}
}
if ! failedThreeDay {
t . Fatalf ( "reports = %#v, want failed 3-day item" , result . Reports )
}
}
2026-06-07 23:38:03 +00:00
func TestRunBatchContinuesAfterNotificationFailure ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
cfg . Notify . Distributor . Enabled = true
2026-06-08 07:04:31 -05:00
cfg . Notify . Distributor . PipelineIDTemplate = "weatherreporter.{artifact_group}"
2026-06-07 23:38:03 +00:00
notifier := & recordingNotifier {
errByReport : map [ report . ID ] error {
report . ThreeDay : errors . New ( "distributor unavailable" ) ,
} ,
}
result , err := RunBatchDetailed ( context . Background ( ) , BatchRequest {
Config : cfg ,
Batch : BatchMorning ,
Now : mustParse ( "2026-05-29T05:00:00-05:00" ) ,
Renderer : & selectiveRenderer { runBody : "# Batch Report\n" } ,
Notifier : notifier ,
} )
if err != nil {
t . Fatalf ( "RunBatchDetailed() error = %v" , err )
}
if result . Total != 3 || result . Succeeded != 2 || result . Failed != 1 {
t . Fatalf ( "summary total/succeeded/failed = %d/%d/%d, want 3/2/1" , result . Total , result . Succeeded , result . Failed )
}
if len ( notifier . requests ) != 3 {
t . Fatalf ( "notification requests = %d, want one per generated report" , len ( notifier . requests ) )
}
var failedThreeDay bool
for _ , item := range result . Reports {
if item . ReportID == report . ThreeDay {
if item . Status == "failed" && strings . Contains ( item . Error , "notify report" ) && strings . Contains ( item . Error , "distributor unavailable" ) {
failedThreeDay = true
}
2026-06-07 23:42:27 +00:00
if item . NotificationStatus != "failed" {
t . Fatalf ( "3-day notification status = %q, want failed" , item . NotificationStatus )
}
2026-06-08 07:04:31 -05:00
if item . NotificationPipelineID != "weatherreporter.three-day" {
t . Fatalf ( "3-day notification pipeline = %q, want weatherreporter.three-day" , item . NotificationPipelineID )
}
2026-06-07 23:42:27 +00:00
if ! strings . Contains ( item . NotificationError , "distributor unavailable" ) {
t . Fatalf ( "3-day notification error = %q, want distributor unavailable" , item . NotificationError )
}
2026-06-07 23:38:03 +00:00
continue
}
if item . Status != "succeeded" {
t . Fatalf ( "report %s status = %s, want succeeded" , item . ReportID , item . Status )
}
2026-06-07 23:42:27 +00:00
if item . NotificationStatus != "accepted" {
t . Fatalf ( "report %s notification status = %q, want accepted" , item . ReportID , item . NotificationStatus )
}
2026-06-08 07:04:31 -05:00
if item . NotificationPipelineID == "" {
t . Fatalf ( "report %s notification pipeline is empty" , item . ReportID )
}
2026-06-07 23:38:03 +00:00
}
if ! failedThreeDay {
t . Fatalf ( "reports = %#v, want notification failure on 3-day item" , result . Reports )
}
}
2026-05-29 18:20:39 +00:00
func TestRunBatchUsesOutputDirectory ( t * testing . T ) {
server := dailyBundleServer ( t )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
cfg . Workspace . Root = t . TempDir ( )
outputDir := filepath . Join ( t . TempDir ( ) , "reports" )
result , err := RunBatchDetailed ( context . Background ( ) , BatchRequest {
Config : cfg ,
Batch : BatchEvening ,
Now : mustParse ( "2026-05-29T18:00:00-05:00" ) ,
OutputDir : outputDir ,
Renderer : & selectiveRenderer { runBody : "# Tomorrow\n" } ,
} )
if err != nil {
t . Fatalf ( "RunBatchDetailed() error = %v" , err )
}
if result . Failed != 0 || len ( result . Reports ) != 1 {
t . Fatalf ( "summary = %#v, want one successful report" , result )
}
want := filepath . Join ( outputDir , "tomorrow.md" )
if result . Reports [ 0 ] . OutputPath != want {
t . Fatalf ( "OutputPath = %q, want %q" , result . Reports [ 0 ] . OutputPath , want )
}
if _ , err := os . Stat ( want ) ; err != nil {
t . Fatalf ( "expected output copy %q: %v" , want , err )
}
}
2026-05-29 17:17:50 +00:00
func mustParse ( value string ) time . Time {
parsed , err := time . Parse ( time . RFC3339 , value )
if err != nil {
panic ( err )
}
return parsed
}
2026-05-29 17:32:57 +00:00
2026-05-29 20:44:25 +00:00
func dailyTestConfig ( t * testing . T , server * httptest . Server ) config . Config {
t . Helper ( )
cfg := config . Defaults ( )
cfg . WeatherAPI . BaseURL = server . URL + "/"
cfg . WeatherAPI . Timezone = "America/Chicago"
return cfg
}
func assertPathsExist ( t * testing . T , paths ... string ) {
t . Helper ( )
for _ , path := range paths {
if _ , err := os . Stat ( path ) ; err != nil {
t . Fatalf ( "expected artifact %q: %v" , path , err )
}
}
}
2026-06-09 21:33:34 +00:00
func savePriorRun ( t * testing . T , store state . Store , resolved report . Resolved , snapshot module . Snapshot ) {
2026-06-09 21:20:53 +00:00
t . Helper ( )
moduleSnapshotPath , err := store . SaveModuleSnapshot ( context . Background ( ) , resolved , snapshot )
if err != nil {
t . Fatalf ( "SaveModuleSnapshot() error = %v" , err )
}
paths , err := store . Paths ( resolved )
if err != nil {
t . Fatalf ( "Paths() error = %v" , err )
}
2026-06-09 21:33:34 +00:00
_ , err = store . SaveMetadata ( context . Background ( ) , state . BuildMetadataFromBriefingMetadata ( resolved , appBriefingMetadata ( resolved ) , state . ArtifactPaths {
2026-06-09 21:20:53 +00:00
ModuleSnapshot : moduleSnapshotPath ,
Metadata : paths . Metadata ,
DataPackage : paths . DataPackage ,
Preflight : paths . Preflight ,
RenderedReport : paths . RenderedReport ,
} ) )
if err != nil {
t . Fatalf ( "SaveMetadata() error = %v" , err )
}
}
func priorDailyModuleSnapshot ( t * testing . T , resolved report . Resolved ) module . Snapshot {
t . Helper ( )
low := 50
high := 58
precip := 10
snapshot , err := module . NewSnapshot ( [ ] module . Output {
{ ID : module . DerivedDailySummary , StanzaName : "derived_daily_summary" , Value : map [ string ] any {
"date" : resolved . ValidPeriod . Start . Format ( timeutil . DateLayout ) ,
"low_temp_f" : low ,
"high_temp_f" : high ,
"max_pop_percent" : precip ,
} } ,
{ ID : module . DerivedDaypartSummaries , StanzaName : "derived_daypart_summaries" , Value : map [ string ] any {
"morning" : map [ string ] any {
"period" : timeutil . Period { Start : resolved . ValidPeriod . Start . Add ( 6 * time . Hour ) , End : resolved . ValidPeriod . Start . Add ( 10 * time . Hour ) } ,
"temp_range_f" : "50-58" ,
} ,
} } ,
{ ID : module . PrecipTiming , StanzaName : "precip_timing" , Value : map [ string ] any {
"max_pop_percent" : precip ,
"max_pop_time" : "6 AM" ,
} } ,
{ ID : module . AlertDigest , StanzaName : "alert_digest" , Value : map [ string ] any { } } ,
} )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
return snapshot
}
func priorOutlookModuleSnapshot ( t * testing . T , date string ) module . Snapshot {
t . Helper ( )
precip := 10
snapshot , err := module . NewSnapshot ( [ ] module . Output {
{ ID : module . DerivedDaypartSummaries , StanzaName : "derived_daypart_summaries" , Value : map [ string ] any {
date + "_morning" : map [ string ] any {
"period" : timeutil . Period { Start : mustParse ( date + "T06:00:00Z" ) , End : mustParse ( date + "T10:00:00Z" ) } ,
"temp_range_f" : "50-58" ,
"max_pop_percent" : precip ,
"max_pop_time" : "6 AM" ,
} ,
} } ,
} )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
return snapshot
}
2026-06-09 21:33:34 +00:00
func appBriefingMetadata ( resolved report . Resolved ) briefing . Metadata {
return briefing . Metadata {
RunID : resolved . Metadata ( ) . RunID ,
ReportID : resolved . Definition . ID ,
Variant : "today" ,
PromptID : resolved . Definition . PromptID ,
GeneratedAt : resolved . GeneratedAt ,
Units : "us" ,
Timezone : resolved . Timezone ,
ValidPeriod : resolved . ValidPeriod ,
2026-05-29 18:16:10 +00:00
}
}
2026-05-29 17:32:57 +00:00
type recordingRenderer struct {
2026-05-29 17:47:42 +00:00
renderCalls int
runCalls int
renderRequest scriptorium . RenderRequest
runRequest scriptorium . RunRequest
renderResult * scriptorium . RenderResult
runResult * scriptorium . RunResult
err error
runErr error
runBody string
2026-05-29 17:32:57 +00:00
}
2026-06-09 21:12:13 +00:00
type recordingStore struct {
state . Store
calls [ ] string
}
func ( s * recordingStore ) SaveModuleSnapshot ( ctx context . Context , resolved report . Resolved , snapshot module . Snapshot ) ( string , error ) {
s . calls = append ( s . calls , "module_snapshot" )
return s . Store . SaveModuleSnapshot ( ctx , resolved , snapshot )
}
func ( s * recordingStore ) SaveDataPackage ( ctx context . Context , resolved report . Resolved , pkg promptinput . Package ) ( string , error ) {
s . calls = append ( s . calls , "data_package" )
return s . Store . SaveDataPackage ( ctx , resolved , pkg )
}
func ( s * recordingStore ) SavePreflight ( ctx context . Context , resolved report . Resolved , artifact state . PreflightArtifact ) ( string , error ) {
s . calls = append ( s . calls , "preflight" )
return s . Store . SavePreflight ( ctx , resolved , artifact )
}
func ( s * recordingStore ) PrepareRenderedReport ( ctx context . Context , resolved report . Resolved ) ( string , error ) {
s . calls = append ( s . calls , "prepare_report" )
return s . Store . PrepareRenderedReport ( ctx , resolved )
}
func ( s * recordingStore ) SaveMetadata ( ctx context . Context , metadata state . Metadata ) ( string , error ) {
s . calls = append ( s . calls , "metadata" )
return s . Store . SaveMetadata ( ctx , metadata )
}
2026-06-07 23:38:03 +00:00
func successfulRenderer ( body string ) * recordingRenderer {
return & recordingRenderer {
renderResult : & scriptorium . RenderResult { ExitCode : 0 } ,
runResult : & scriptorium . RunResult { ExitCode : 0 } ,
runBody : body ,
}
}
2026-05-29 18:20:39 +00:00
type selectiveRenderer struct {
renderCalls int
runCalls int
failRenderPrompt string
runBody string
}
2026-06-07 23:38:03 +00:00
type recordingNotifier struct {
requests [ ] NotificationRequest
result * NotificationResult
err error
errByReport map [ report . ID ] error
}
func ( n * recordingNotifier ) Notify ( _ context . Context , req NotificationRequest ) ( * NotificationResult , error ) {
n . requests = append ( n . requests , req )
if err := n . errByReport [ req . ReportID ] ; err != nil {
return nil , err
}
if n . err != nil {
return nil , n . err
}
if n . result != nil {
result := * n . result
if result . BundleID == "" {
result . BundleID = req . BundleID
}
if result . IdempotencyKey == "" {
result . IdempotencyKey = req . IdempotencyKey
}
2026-06-08 07:04:31 -05:00
if result . PipelineID == "" {
result . PipelineID = req . PipelineID
}
2026-06-07 23:38:03 +00:00
return & result , nil
}
return & NotificationResult {
2026-06-08 07:04:31 -05:00
PipelineID : req . PipelineID ,
2026-06-07 23:38:03 +00:00
BundleID : req . BundleID ,
IdempotencyKey : req . IdempotencyKey ,
Status : "accepted" ,
2026-06-07 20:21:26 -05:00
UploadStatus : "accepted" ,
2026-06-07 23:38:03 +00:00
} , nil
}
2026-05-29 18:20:39 +00:00
func ( r * selectiveRenderer ) Render ( _ context . Context , req scriptorium . RenderRequest ) ( * scriptorium . RenderResult , error ) {
r . renderCalls ++
if req . PromptID == r . failRenderPrompt {
return & scriptorium . RenderResult { ExitCode : 1 , Stderr : "render failed" } , errors . New ( "render failed" )
}
return & scriptorium . RenderResult { ExitCode : 0 } , nil
}
func ( r * selectiveRenderer ) Run ( _ context . Context , req scriptorium . RunRequest ) ( * scriptorium . RunResult , error ) {
r . runCalls ++
if r . runBody != "" {
if err := os . WriteFile ( req . OutputPath , [ ] byte ( r . runBody ) , 0 o600 ) ; err != nil {
return nil , err
}
}
return & scriptorium . RunResult { ExitCode : 0 , OutputPath : req . OutputPath } , nil
}
2026-05-29 17:32:57 +00:00
func ( r * recordingRenderer ) Render ( _ context . Context , req scriptorium . RenderRequest ) ( * scriptorium . RenderResult , error ) {
2026-05-29 17:47:42 +00:00
r . renderCalls ++
r . renderRequest = req
return r . renderResult , r . err
}
func ( r * recordingRenderer ) Run ( _ context . Context , req scriptorium . RunRequest ) ( * scriptorium . RunResult , error ) {
r . runCalls ++
r . runRequest = req
if r . runBody != "" {
if err := os . WriteFile ( req . OutputPath , [ ] byte ( r . runBody ) , 0 o600 ) ; err != nil {
return nil , err
}
}
if r . runResult != nil {
r . runResult . OutputPath = req . OutputPath
}
return r . runResult , r . runErr
2026-05-29 17:32:57 +00:00
}