Add app collection seam
This commit is contained in:
@@ -53,6 +53,7 @@ type GenerateRequest struct {
|
||||
Date time.Time
|
||||
StormStart time.Time
|
||||
StormEnd time.Time
|
||||
Collector Collector
|
||||
Notifier Notifier
|
||||
}
|
||||
|
||||
@@ -61,6 +62,7 @@ type BatchRequest struct {
|
||||
Batch BatchKind
|
||||
Now time.Time
|
||||
OutputDir string
|
||||
Collector Collector
|
||||
Renderer Renderer
|
||||
Store state.Store
|
||||
Notifier Notifier
|
||||
@@ -85,6 +87,7 @@ type ReportRequest struct {
|
||||
Config config.Config
|
||||
Resolved report.Resolved
|
||||
OutputPath string
|
||||
Collector Collector
|
||||
Renderer Renderer
|
||||
Store state.Store
|
||||
Notifier Notifier
|
||||
@@ -161,6 +164,16 @@ type Renderer interface {
|
||||
StructuredRun(context.Context, scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error)
|
||||
}
|
||||
|
||||
type Collector interface {
|
||||
Run(context.Context, collect.Request) (*collect.Result, error)
|
||||
}
|
||||
|
||||
type defaultCollector struct{}
|
||||
|
||||
func (defaultCollector) Run(ctx context.Context, req collect.Request) (*collect.Result, error) {
|
||||
return collect.Run(ctx, req)
|
||||
}
|
||||
|
||||
type Notifier interface {
|
||||
Notify(context.Context, NotificationRequest) (*NotificationResult, error)
|
||||
}
|
||||
@@ -224,6 +237,7 @@ func Generate(ctx context.Context, req GenerateRequest) error {
|
||||
Config: req.Config,
|
||||
Resolved: resolved,
|
||||
OutputPath: req.OutputPath,
|
||||
Collector: req.Collector,
|
||||
Notifier: req.Notifier,
|
||||
})
|
||||
return err
|
||||
@@ -280,6 +294,7 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
|
||||
Config: req.Config,
|
||||
Resolved: resolved,
|
||||
OutputPath: outputPath,
|
||||
Collector: req.Collector,
|
||||
Renderer: req.Renderer,
|
||||
Store: store,
|
||||
Notifier: req.Notifier,
|
||||
@@ -394,10 +409,23 @@ func reportRegistry(cfg config.Config) (report.Registry, error) {
|
||||
}
|
||||
|
||||
func FetchBundle(ctx context.Context, req FetchBundleRequest) (*weatherdata.Bundle, error) {
|
||||
result, err := collect.Run(ctx, collect.Request{Config: req.Config})
|
||||
return collectBundle(ctx, req.Config, nil)
|
||||
}
|
||||
|
||||
func collectBundle(ctx context.Context, cfg config.Config, collector Collector) (*weatherdata.Bundle, error) {
|
||||
if collector == nil {
|
||||
collector = defaultCollector{}
|
||||
}
|
||||
result, err := collector.Run(ctx, collect.Request{Config: cfg})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, fmt.Errorf("collect weather bundle: collector returned nil result")
|
||||
}
|
||||
if result.Bundle == nil {
|
||||
return nil, fmt.Errorf("collect weather bundle: collector returned nil bundle")
|
||||
}
|
||||
return result.Bundle, nil
|
||||
}
|
||||
|
||||
@@ -433,7 +461,7 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bundle, err := FetchBundle(ctx, FetchBundleRequest{Config: req.Config})
|
||||
bundle, err := collectBundle(ctx, req.Config, req.Collector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/collect"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
|
||||
@@ -81,6 +82,61 @@ func TestFetchAndSaveBundleRequiresOutputPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateUsesProvidedCollector(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.BaseURL = ""
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
collector := &recordingCollector{err: errors.New("provided collector failed")}
|
||||
|
||||
err := Generate(context.Background(), GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Collector: collector,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Generate() error = nil, want collector error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "provided collector failed") {
|
||||
t.Fatalf("Generate() error = %q, want provided collector error", err.Error())
|
||||
}
|
||||
if len(collector.requests) != 1 {
|
||||
t.Fatalf("collector requests = %d, want 1", len(collector.requests))
|
||||
}
|
||||
if collector.requests[0].Config.WeatherAPI.BaseURL != "" {
|
||||
t.Fatalf("collector base URL = %q, want request config", collector.requests[0].Config.WeatherAPI.BaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCollectionFailureStopsBeforeReportExecution(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.BaseURL = ""
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
markerPath := filepath.Join(t.TempDir(), "scriptorium-called")
|
||||
binaryPath := filepath.Join(t.TempDir(), "scriptorium")
|
||||
script := fmt.Sprintf("#!/bin/sh\nprintf called > %q\nexit 0\n", markerPath)
|
||||
if err := os.WriteFile(binaryPath, []byte(script), 0o755); err != nil {
|
||||
t.Fatalf("write scriptorium marker script: %v", err)
|
||||
}
|
||||
cfg.Scriptorium.Binary = binaryPath
|
||||
collector := &recordingCollector{err: errors.New("collection unavailable")}
|
||||
|
||||
err := Generate(context.Background(), GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Collector: collector,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Generate() error = nil, want collector error")
|
||||
}
|
||||
if _, statErr := os.Stat(markerPath); !errors.Is(statErr, os.ErrNotExist) {
|
||||
t.Fatalf("scriptorium marker stat error = %v, want marker absent after collection failure", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyWorkspaceConfig(t, server)
|
||||
@@ -2318,6 +2374,39 @@ func TestBatchOutputPathUsesHourlyOutputName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunBatchDetailedUsesProvidedCollector(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.BaseURL = ""
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
collector := &recordingCollector{err: errors.New("batch collector failed")}
|
||||
renderer := &recordingRenderer{}
|
||||
|
||||
result, err := RunBatchDetailed(context.Background(), BatchRequest{
|
||||
Config: cfg,
|
||||
Batch: BatchMorning,
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Collector: collector,
|
||||
Renderer: renderer,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunBatchDetailed() error = %v", err)
|
||||
}
|
||||
if result.Total == 0 || result.Failed != result.Total {
|
||||
t.Fatalf("batch result = %#v, want every report failed by collector", result)
|
||||
}
|
||||
if len(collector.requests) != result.Total {
|
||||
t.Fatalf("collector requests = %d, want %d", len(collector.requests), result.Total)
|
||||
}
|
||||
for _, reportResult := range result.Reports {
|
||||
if !strings.Contains(reportResult.Error, "batch collector failed") {
|
||||
t.Fatalf("report error = %q, want fake collector error", reportResult.Error)
|
||||
}
|
||||
}
|
||||
if renderer.renderCalls != 0 || renderer.runCalls != 0 || renderer.structuredRunCalls != 0 {
|
||||
t.Fatalf("renderer calls render=%d run=%d structured=%d, want none after collection failure", renderer.renderCalls, renderer.runCalls, renderer.structuredRunCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func mustParse(value string) time.Time {
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
@@ -2790,6 +2879,20 @@ type recordingRenderer struct {
|
||||
structuredRunBody string
|
||||
}
|
||||
|
||||
type recordingCollector struct {
|
||||
result *collect.Result
|
||||
err error
|
||||
requests []collect.Request
|
||||
}
|
||||
|
||||
func (c *recordingCollector) Run(_ context.Context, req collect.Request) (*collect.Result, error) {
|
||||
c.requests = append(c.requests, req)
|
||||
if c.err != nil {
|
||||
return nil, c.err
|
||||
}
|
||||
return c.result, nil
|
||||
}
|
||||
|
||||
type recordingStore struct {
|
||||
state.Store
|
||||
calls []string
|
||||
|
||||
Reference in New Issue
Block a user