Add app collection seam

This commit is contained in:
2026-06-17 15:46:01 +00:00
parent b308ff4d6b
commit 0ef6cd567e
2 changed files with 133 additions and 2 deletions

View File

@@ -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
}

View File

@@ -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