Add app collection seam
This commit is contained in:
@@ -53,6 +53,7 @@ type GenerateRequest struct {
|
|||||||
Date time.Time
|
Date time.Time
|
||||||
StormStart time.Time
|
StormStart time.Time
|
||||||
StormEnd time.Time
|
StormEnd time.Time
|
||||||
|
Collector Collector
|
||||||
Notifier Notifier
|
Notifier Notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ type BatchRequest struct {
|
|||||||
Batch BatchKind
|
Batch BatchKind
|
||||||
Now time.Time
|
Now time.Time
|
||||||
OutputDir string
|
OutputDir string
|
||||||
|
Collector Collector
|
||||||
Renderer Renderer
|
Renderer Renderer
|
||||||
Store state.Store
|
Store state.Store
|
||||||
Notifier Notifier
|
Notifier Notifier
|
||||||
@@ -85,6 +87,7 @@ type ReportRequest struct {
|
|||||||
Config config.Config
|
Config config.Config
|
||||||
Resolved report.Resolved
|
Resolved report.Resolved
|
||||||
OutputPath string
|
OutputPath string
|
||||||
|
Collector Collector
|
||||||
Renderer Renderer
|
Renderer Renderer
|
||||||
Store state.Store
|
Store state.Store
|
||||||
Notifier Notifier
|
Notifier Notifier
|
||||||
@@ -161,6 +164,16 @@ type Renderer interface {
|
|||||||
StructuredRun(context.Context, scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error)
|
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 {
|
type Notifier interface {
|
||||||
Notify(context.Context, NotificationRequest) (*NotificationResult, error)
|
Notify(context.Context, NotificationRequest) (*NotificationResult, error)
|
||||||
}
|
}
|
||||||
@@ -224,6 +237,7 @@ func Generate(ctx context.Context, req GenerateRequest) error {
|
|||||||
Config: req.Config,
|
Config: req.Config,
|
||||||
Resolved: resolved,
|
Resolved: resolved,
|
||||||
OutputPath: req.OutputPath,
|
OutputPath: req.OutputPath,
|
||||||
|
Collector: req.Collector,
|
||||||
Notifier: req.Notifier,
|
Notifier: req.Notifier,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
@@ -280,6 +294,7 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
|
|||||||
Config: req.Config,
|
Config: req.Config,
|
||||||
Resolved: resolved,
|
Resolved: resolved,
|
||||||
OutputPath: outputPath,
|
OutputPath: outputPath,
|
||||||
|
Collector: req.Collector,
|
||||||
Renderer: req.Renderer,
|
Renderer: req.Renderer,
|
||||||
Store: store,
|
Store: store,
|
||||||
Notifier: req.Notifier,
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return result.Bundle, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +461,7 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bundle, err := FetchBundle(ctx, FetchBundleRequest{Config: req.Config})
|
bundle, err := collectBundle(ctx, req.Config, req.Collector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
"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/config"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
|
"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) {
|
func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||||
server := dailyBundleServer(t)
|
server := dailyBundleServer(t)
|
||||||
cfg := dailyWorkspaceConfig(t, server)
|
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 {
|
func mustParse(value string) time.Time {
|
||||||
parsed, err := time.Parse(time.RFC3339, value)
|
parsed, err := time.Parse(time.RFC3339, value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2790,6 +2879,20 @@ type recordingRenderer struct {
|
|||||||
structuredRunBody string
|
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 {
|
type recordingStore struct {
|
||||||
state.Store
|
state.Store
|
||||||
calls []string
|
calls []string
|
||||||
|
|||||||
Reference in New Issue
Block a user