Generate Daily reports through scriptorium

This commit is contained in:
2026-05-29 17:47:42 +00:00
parent 7ac73f758b
commit b17a3591e0
14 changed files with 543 additions and 113 deletions

View File

@@ -133,7 +133,7 @@ func TestGenerateDailyBriefingDefaultPath(t *testing.T) {
}
}
func TestPrepareDailyReportWritesDataPackageAndPreflight(t *testing.T) {
func TestGenerateDailyReportWritesReportAndPreflight(t *testing.T) {
server := dailyBundleServer(t)
cfg := config.Defaults()
cfg.WeatherAPI.BaseURL = server.URL + "/"
@@ -148,42 +148,58 @@ func TestPrepareDailyReportWritesDataPackageAndPreflight(t *testing.T) {
t.Fatalf("ResolveGenerate() error = %v", err)
}
renderer := &recordingRenderer{
result: &scriptorium.RenderResult{
renderResult: &scriptorium.RenderResult{
Command: []string{"scriptorium", "render"},
Stdout: `{"prepared":true}`,
ExitCode: 0,
},
runResult: &scriptorium.RunResult{
Command: []string{"scriptorium", "run"},
Stderr: "wrote report",
ExitCode: 0,
OutputPath: "",
},
runBody: "# Daily Report\n\nRain this morning.\n",
}
dataPackagePath := filepath.Join(t.TempDir(), "daily.data_package.json")
outputPath := filepath.Join(t.TempDir(), "daily.md")
result, err := PrepareDailyReport(context.Background(), DailyPreparationRequest{
Config: cfg,
Resolved: resolved,
DataPackagePath: dataPackagePath,
Renderer: renderer,
result, err := GenerateDailyReport(context.Background(), DailyReportRequest{
Config: cfg,
Resolved: resolved,
OutputPath: outputPath,
Renderer: renderer,
})
if err != nil {
t.Fatalf("PrepareDailyReport() error = %v", err)
t.Fatalf("GenerateDailyReport() error = %v", err)
}
if renderer.calls != 1 {
t.Fatalf("renderer calls = %d, want 1", renderer.calls)
if renderer.renderCalls != 1 {
t.Fatalf("render calls = %d, want 1", renderer.renderCalls)
}
if renderer.request.PromptID != "weather.daily_report" {
t.Fatalf("PromptID = %q, want weather.daily_report", renderer.request.PromptID)
if renderer.runCalls != 1 {
t.Fatalf("run calls = %d, want 1", renderer.runCalls)
}
if renderer.request.DataPackagePath != result.DataPackagePath {
t.Fatalf("DataPackagePath = %q, want managed path %q", renderer.request.DataPackagePath, result.DataPackagePath)
if renderer.renderRequest.PromptID != "weather.daily_report" {
t.Fatalf("render PromptID = %q, want weather.daily_report", renderer.renderRequest.PromptID)
}
for _, path := range []string{result.BriefingPath, result.DataPackagePath, result.PreflightPath, result.MetadataPath} {
if renderer.renderRequest.DataPackagePath != result.DataPackagePath {
t.Fatalf("render DataPackagePath = %q, want managed path %q", renderer.renderRequest.DataPackagePath, result.DataPackagePath)
}
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)
}
for _, path := range []string{result.BriefingPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath} {
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected artifact %q: %v", path, err)
}
}
if _, err := os.Stat(dataPackagePath); err != nil {
t.Fatalf("expected requested data package copy %q: %v", dataPackagePath, err)
if _, err := os.Stat(outputPath); err != nil {
t.Fatalf("expected requested report output %q: %v", outputPath, err)
}
data, err := os.ReadFile(dataPackagePath)
data, err := os.ReadFile(result.DataPackagePath)
if err != nil {
t.Fatalf("read data package: %v", err)
}
@@ -203,9 +219,19 @@ func TestPrepareDailyReportWritesDataPackageAndPreflight(t *testing.T) {
if result.Metadata.BriefingPath != result.BriefingPath || result.Metadata.DataPackagePath != result.DataPackagePath {
t.Fatalf("metadata does not link artifact paths: %#v", result.Metadata)
}
if result.Metadata.RenderedReportPath != result.ReportPath {
t.Fatalf("metadata rendered report path = %q, want %q", result.Metadata.RenderedReportPath, result.ReportPath)
}
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))
}
}
func TestPrepareDailyReportPersistsFailedPreflight(t *testing.T) {
func TestGenerateDailyReportPersistsFailedPreflight(t *testing.T) {
server := dailyBundleServer(t)
cfg := config.Defaults()
cfg.WeatherAPI.BaseURL = server.URL + "/"
@@ -220,7 +246,7 @@ func TestPrepareDailyReportPersistsFailedPreflight(t *testing.T) {
t.Fatalf("ResolveGenerate() error = %v", err)
}
renderer := &recordingRenderer{
result: &scriptorium.RenderResult{
renderResult: &scriptorium.RenderResult{
Command: []string{"scriptorium", "render"},
Stderr: "render failed",
ExitCode: 1,
@@ -228,13 +254,13 @@ func TestPrepareDailyReportPersistsFailedPreflight(t *testing.T) {
err: errors.New("scriptorium render exited with code 1: render failed"),
}
_, err = PrepareDailyReport(context.Background(), DailyPreparationRequest{
_, err = GenerateDailyReport(context.Background(), DailyReportRequest{
Config: cfg,
Resolved: resolved,
Renderer: renderer,
})
if err == nil {
t.Fatal("PrepareDailyReport() error = nil, want render error")
t.Fatal("GenerateDailyReport() error = nil, want render error")
}
store, err := state.NewFilesystemStore(cfg.Workspace)
if err != nil {
@@ -255,6 +281,60 @@ func TestPrepareDailyReportPersistsFailedPreflight(t *testing.T) {
if _, err := os.Stat(paths.Metadata); err != nil {
t.Fatalf("expected metadata for failed preflight %q: %v", paths.Metadata, err)
}
if renderer.runCalls != 0 {
t.Fatalf("run calls = %d, want none after failed preflight", renderer.runCalls)
}
}
func TestGenerateDailyReportReturnsRunErrorAfterPreflight(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{
Stderr: "validation failed",
ExitCode: 2,
},
runErr: errors.New("scriptorium run exited with code 2: validation failed"),
runBody: "# Daily Report\n",
}
_, err = GenerateDailyReport(context.Background(), DailyReportRequest{
Config: cfg,
Resolved: resolved,
Renderer: renderer,
})
if err == nil {
t.Fatal("GenerateDailyReport() error = nil, want run error")
}
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)
}
}
func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
@@ -356,14 +436,33 @@ func mustParse(value string) time.Time {
}
type recordingRenderer struct {
calls int
request scriptorium.RenderRequest
result *scriptorium.RenderResult
err error
renderCalls int
runCalls int
renderRequest scriptorium.RenderRequest
runRequest scriptorium.RunRequest
renderResult *scriptorium.RenderResult
runResult *scriptorium.RunResult
err error
runErr error
runBody string
}
func (r *recordingRenderer) Render(_ context.Context, req scriptorium.RenderRequest) (*scriptorium.RenderResult, error) {
r.calls++
r.request = req
return r.result, r.err
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), 0o600); err != nil {
return nil, err
}
}
if r.runResult != nil {
r.runResult.OutputPath = req.OutputPath
}
return r.runResult, r.runErr
}