Use Promptkit for single report generation
This commit is contained in:
@@ -13,10 +13,51 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
)
|
||||
|
||||
func testRunner() Runner {
|
||||
return testRunnerWithClock(fixedClock())
|
||||
}
|
||||
|
||||
func testRunnerWithClock(clock timeutil.Clock) Runner {
|
||||
return Runner{Clock: clock, ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
|
||||
return cliPromptExecutor{}, nil
|
||||
}}
|
||||
}
|
||||
|
||||
type cliPromptExecutor struct{}
|
||||
|
||||
func (cliPromptExecutor) InspectPrompt(_ context.Context, id string, version string) (promptexec.PromptInspection, error) {
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(id, "weather."), "_generated_text")
|
||||
return promptexec.PromptInspection{
|
||||
PromptID: id, PromptVersion: version, PromptHash: "prompt-hash", DefaultProfileID: "test-profile",
|
||||
Inputs: []promptexec.InputDefinition{{Name: "data_package", Required: true, ContentType: "application/yaml"}},
|
||||
Output: promptexec.OutputContract{Format: "json", ValidationMode: "json_schema", SchemaPath: name + ".generated_text.schema.json"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cliPromptExecutor) InspectProfile(_ context.Context, id string) (promptexec.ProfileInspection, error) {
|
||||
return promptexec.ProfileInspection{ProfileID: id, BackendID: "test", ModelName: "test-model"}, nil
|
||||
}
|
||||
|
||||
func (cliPromptExecutor) Execute(_ context.Context, request promptexec.ExecuteRequest, callback promptexec.PreparationCallback) (*promptexec.Execution, error) {
|
||||
now := time.Now().UTC()
|
||||
if err := callback(promptexec.Preparation{PromptID: request.PromptID, PromptVersion: request.PromptVersion, PromptHash: "prompt-hash", RenderedPromptHash: "rendered-hash", ProfileID: request.ProfileID, BackendID: "test", ModelName: "test-model", DataPackagePath: request.DataPackagePath, StartedAt: now, EndedAt: now}, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw := []byte(`{"summary": "Showers are possible during the selected day.", "forecast_discussion": ["A front will keep rain chances in the forecast."], "precipitation_timing": "Rain is most likely during the afternoon."}`)
|
||||
if request.PromptID == "weather.today_generated_text" {
|
||||
raw = []byte(`{"summary": "Today starts with showers before improving.", "forecast_discussion": ["Morning showers should taper as drier air arrives.", "Afternoon conditions trend quieter."], "precipitation_timing": "The best rain chance is during the morning."}`)
|
||||
}
|
||||
if request.PromptID == "weather.hourly_generated_text" {
|
||||
raw = []byte(`{"summary":"Storm chances increase through late morning.","forecast_discussion":"A front will keep the region unsettled.","precipitation_timing":"A cold front is moving into the region."}`)
|
||||
}
|
||||
return &promptexec.Execution{RunID: "provider-run", PromptID: request.PromptID, PromptVersion: request.PromptVersion, PromptHash: "prompt-hash", RenderedPromptHash: "rendered-hash", ProfileID: request.ProfileID, BackendID: "test", ModelName: "test-model", GeneratedHash: "generated-hash", StartedAt: now, EndedAt: now, DataPackagePath: request.DataPackagePath, RawOutput: raw, Validation: promptexec.NewValidation(promptexec.ValidationPassed, "json_schema", "generated_text.schema.json", nil)}, nil
|
||||
}
|
||||
|
||||
func TestRunHelpLongFlag(t *testing.T) {
|
||||
output, err := runRootCommand(t, "--help")
|
||||
if err != nil {
|
||||
@@ -73,7 +114,7 @@ func TestRunUnknownCommand(t *testing.T) {
|
||||
func TestRunGenerateTomorrowWritesMarkdownReport(t *testing.T) {
|
||||
fixture := newCLIFixture(t, writeFakeScriptorium)
|
||||
outPath := fixture.path("tomorrow.md")
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
_, err := runTestCommand(t, runner,
|
||||
"generate", "tomorrow",
|
||||
@@ -95,7 +136,7 @@ func TestRunGenerateTomorrowWritesMarkdownReport(t *testing.T) {
|
||||
|
||||
func TestRunEveningGeneratesTomorrowReport(t *testing.T) {
|
||||
fixture := newCLIFixture(t, writeFakeScriptorium)
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
_, err := runTestCommand(t, runner,
|
||||
"run", "evening",
|
||||
@@ -113,7 +154,7 @@ func TestRunEveningGeneratesTomorrowReport(t *testing.T) {
|
||||
|
||||
func TestRunMorningGeneratesTodayAndTomorrow(t *testing.T) {
|
||||
fixture := newCLIFixture(t, writeFakeScriptorium)
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
_, err := runTestCommand(t, runner,
|
||||
"run", "morning",
|
||||
@@ -129,7 +170,7 @@ func TestRunMorningGeneratesTodayAndTomorrow(t *testing.T) {
|
||||
|
||||
func TestRunMorningReportsPartialFailureAndContinues(t *testing.T) {
|
||||
fixture := newCLIFixture(t, writeFailingScriptorium)
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
output, err := runTestCommand(t, runner,
|
||||
"run", "morning",
|
||||
@@ -322,7 +363,7 @@ func TestBatchStatusDoesNotRepeatBatchNotificationErrorPerReport(t *testing.T) {
|
||||
func TestRunEveningUsesOutputDirectoryAndSummary(t *testing.T) {
|
||||
fixture := newCLIFixture(t, writeFakeScriptorium)
|
||||
outputDir := fixture.path("copies")
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
output, err := runTestCommand(t, runner,
|
||||
"run", "evening",
|
||||
@@ -349,7 +390,7 @@ func TestRunEveningUsesOutputDirectoryAndSummary(t *testing.T) {
|
||||
|
||||
func TestRunQuietSuppressesSuccessfulOutput(t *testing.T) {
|
||||
fixture := newCLIFixture(t, writeFakeScriptorium)
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
output, err := runTestCommand(t, runner,
|
||||
"run", "evening",
|
||||
@@ -390,7 +431,7 @@ func TestRunEveningReportsOmitsPerReportNotification(t *testing.T) {
|
||||
t.Setenv("CLI_DISTRIBUTOR_TOKEN", "cli-secret-token")
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
err := runner.Run(context.Background(), []string{
|
||||
"run", "evening",
|
||||
@@ -442,7 +483,7 @@ func TestRunEveningReportsDoesNotRequirePerReportDistributorToken(t *testing.T)
|
||||
configPath := writeTestConfigWithDisabledBatchDistributor(t, server, scriptoriumPath, workspaceRoot, distributorServer.URL)
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
err := runner.Run(context.Background(), []string{
|
||||
"run", "evening",
|
||||
@@ -490,7 +531,7 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
|
||||
outPath := fixture.path("daily.md")
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
err := runner.Run(context.Background(), []string{
|
||||
"generate", "daily",
|
||||
@@ -524,13 +565,13 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
|
||||
!strings.Contains(string(data), "timezone: UTC") {
|
||||
t.Fatalf("data package missing configured location with overridden timezone:\n%s", string(data))
|
||||
}
|
||||
preflightPath := oneArtifact(t, fixture.workspaceRoot, "preflight", "daily", "2026-05-29", "render.*.json")
|
||||
preflight, err := os.ReadFile(preflightPath)
|
||||
preparationPath := oneArtifact(t, fixture.workspaceRoot, "preflight", "daily", "2026-05-29", "prompt_preparation.*.json")
|
||||
preparation, err := os.ReadFile(preparationPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read preflight: %v", err)
|
||||
t.Fatalf("read preparation: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(preflight), `ok`) {
|
||||
t.Fatalf("preflight missing fake render output:\n%s", string(preflight))
|
||||
if !strings.Contains(string(preparation), `"status": "succeeded"`) {
|
||||
t.Fatalf("preparation missing successful status:\n%s", string(preparation))
|
||||
}
|
||||
_ = oneArtifact(t, fixture.workspaceRoot, "reports", "daily", "2026-05-29", "report.*.md")
|
||||
rawGeneratedTextPath := oneArtifact(t, fixture.workspaceRoot, "snapshots", "daily", "2026-05-29", "generated_text_raw.*.json")
|
||||
@@ -548,7 +589,7 @@ func TestRunGenerateTodayWritesGeneratedTextReport(t *testing.T) {
|
||||
outPath := fixture.path("today.md")
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
err := runner.Run(context.Background(), []string{
|
||||
"generate", "today",
|
||||
@@ -612,7 +653,7 @@ func TestRunGenerateHourlyWritesGeneratedTextReport(t *testing.T) {
|
||||
outPath := fixture.path("hourly.md")
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
runner := Runner{Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 11, 0, 0, 0, time.UTC)}}
|
||||
runner := testRunnerWithClock(timeutil.FixedClock{Time: time.Date(2026, 5, 29, 11, 0, 0, 0, time.UTC)})
|
||||
|
||||
err := runner.Run(context.Background(), []string{
|
||||
"generate", "hourly",
|
||||
@@ -650,7 +691,7 @@ func TestRunGenerateHourlyWritesGeneratedTextReport(t *testing.T) {
|
||||
validatedGeneratedTextPath := oneArtifact(t, fixture.workspaceRoot, "snapshots", "hourly", "2026-05-29", "generated_text.*.json")
|
||||
renderContextPath := oneArtifact(t, fixture.workspaceRoot, "snapshots", "hourly", "2026-05-29", "render_context.*.json")
|
||||
managedReportPath := oneArtifact(t, fixture.workspaceRoot, "reports", "hourly", "2026-05-29", "report.*.md")
|
||||
assertFileContains(t, rawGeneratedTextPath, `"summary": " Storm chances increase through late morning. "`)
|
||||
assertFileContains(t, rawGeneratedTextPath, `"summary":"Storm chances increase through late morning."`)
|
||||
assertFileContains(t, validatedGeneratedTextPath, `"summary":"Storm chances increase through late morning."`)
|
||||
assertFileContains(t, renderContextPath, `"Report": {`)
|
||||
assertFileContains(t, renderContextPath, `"Title": "Hourly Report"`)
|
||||
@@ -663,7 +704,7 @@ func TestRunGenerateHourlyWritesGeneratedTextReport(t *testing.T) {
|
||||
func TestRunGenerateQuietSuppressesSuccessfulOutput(t *testing.T) {
|
||||
fixture := newCLIFixture(t, writeStructuredOutputScriptorium)
|
||||
outPath := fixture.path("today.md")
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
output, err := runTestCommand(t, runner,
|
||||
"generate", "today",
|
||||
@@ -684,7 +725,7 @@ func TestRunGenerateQuietSuppressesSuccessfulOutput(t *testing.T) {
|
||||
func TestRunGeneratePreRunErrorEmitsNoJSON(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
err := runner.Run(context.Background(), []string{"generate", "daily"}, &stdout, &stderr)
|
||||
if err == nil {
|
||||
@@ -706,7 +747,7 @@ func TestRunGenerateNotificationFailureEmitsFailureSummary(t *testing.T) {
|
||||
workspaceRoot := filepath.Join(tempDir, "workspace")
|
||||
configPath := writeTestConfigWithDistributor(t, server, scriptoriumPath, workspaceRoot, distributorServer.URL)
|
||||
t.Setenv("CLI_DISTRIBUTOR_TOKEN", "")
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
output, err := runTestCommand(t, runner,
|
||||
"generate", "daily",
|
||||
@@ -733,7 +774,7 @@ func TestRunGenerateNotificationFailureEmitsFailureSummary(t *testing.T) {
|
||||
|
||||
func TestRunInspectTodayArtifacts(t *testing.T) {
|
||||
fixture := newCLIFixture(t, writeFakeScriptorium)
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
@@ -778,7 +819,7 @@ func TestRunInspectTodayArtifacts(t *testing.T) {
|
||||
|
||||
func TestRunInspectGeneratedArtifacts(t *testing.T) {
|
||||
fixture := newCLIFixture(t, writeFakeScriptorium)
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
@@ -826,7 +867,7 @@ func TestRunInspectMissingMetadata(t *testing.T) {
|
||||
configPath := writeWorkspaceConfig(t, filepath.Join(tempDir, "workspace"))
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
err := runner.Run(context.Background(), []string{"inspect", "metadata", "--config", configPath, "missing"}, &stdout, &stderr)
|
||||
if err == nil {
|
||||
@@ -842,7 +883,7 @@ func TestRunInspectRejectsQuiet(t *testing.T) {
|
||||
configPath := writeWorkspaceConfig(t, filepath.Join(tempDir, "workspace"))
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
err := runner.Run(context.Background(), []string{"inspect", "reports", "--config", configPath, "--quiet"}, &stdout, &stderr)
|
||||
if err == nil {
|
||||
@@ -856,7 +897,7 @@ func TestRunInspectRejectsQuiet(t *testing.T) {
|
||||
func TestRunInspectRunCommandsParseRunIDAndConfig(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
configPath := writeWorkspaceConfig(t, filepath.Join(tempDir, "workspace"))
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
commands := []string{"metadata", "modules", "data-package", "prior", "sources"}
|
||||
|
||||
for _, command := range commands {
|
||||
@@ -889,7 +930,7 @@ func TestRunInspectRunCommandsParseRunIDAndConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateCommands(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
@@ -915,7 +956,7 @@ func TestResolveGenerateCommands(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateSupportsEveryReportCommandName(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
for _, name := range report.CommandNames() {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
args := []string{name}
|
||||
@@ -942,7 +983,7 @@ func TestResolveGenerateSupportsEveryReportCommandName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateHourlyAppliesSharedFlags(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
configPath := writeConfigFile(t, "weather_api:\n units: metric\n timezone: UTC\n")
|
||||
|
||||
req, err := runner.resolveGenerate([]string{"hourly", "--config", configPath, "--units", "us", "--tz", "America/Chicago", "--out", "./hourly.md"})
|
||||
@@ -968,7 +1009,7 @@ func TestResolveGenerateHourlyAppliesSharedFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateHourlyRejectsDateAndStormBounds(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
for _, args := range [][]string{
|
||||
{"hourly", "--date", "2026-05-29"},
|
||||
@@ -988,7 +1029,7 @@ func TestResolveGenerateHourlyRejectsDateAndStormBounds(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateDailyRequiresDate(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
req, err := runner.resolveGenerate([]string{"daily"})
|
||||
if err == nil {
|
||||
@@ -1003,7 +1044,7 @@ func TestResolveGenerateDailyRequiresDate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateDailyRejectsMalformedDate(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
_, err := runner.resolveGenerate([]string{"daily", "--date", "bad-date"})
|
||||
if err == nil {
|
||||
@@ -1015,7 +1056,7 @@ func TestResolveGenerateDailyRejectsMalformedDate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateDailyParsesDate(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
req, err := runner.resolveGenerate([]string{"daily", "--date", "2026-05-29"})
|
||||
if err != nil {
|
||||
@@ -1031,7 +1072,7 @@ func TestResolveGenerateDailyParsesDate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateTodayDate(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
defaultReq, err := runner.resolveGenerate([]string{"today"})
|
||||
if err != nil {
|
||||
@@ -1064,7 +1105,7 @@ func TestResolveGenerateTodayDate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateAppliesSharedFlags(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
|
||||
req, err := runner.resolveGenerate([]string{"daily", "--date", "2026-05-29", "--units", "metric", "--tz", "UTC", "--out", "./daily.md"})
|
||||
if err != nil {
|
||||
@@ -1083,7 +1124,7 @@ func TestResolveGenerateAppliesSharedFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateRejectsRetiredHourlyCommand(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
retired := strings.Join([]string{"near", "term"}, "-")
|
||||
|
||||
_, err := runner.resolveGenerate([]string{retired})
|
||||
@@ -1096,7 +1137,7 @@ func TestResolveGenerateRejectsRetiredHourlyCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveGenerateRejectsRetiredReports(t *testing.T) {
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
runner := testRunner()
|
||||
for _, name := range []string{"three-day", "weekend", "storm"} {
|
||||
if _, err := runner.resolveGenerate([]string{name}); err == nil {
|
||||
t.Fatalf("resolveGenerate(%q) error = nil, want unknown report", name)
|
||||
|
||||
Reference in New Issue
Block a user