Complete comparison command output
This commit is contained in:
@@ -3,6 +3,7 @@ package cli
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/comparison"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
)
|
||||
@@ -175,13 +177,110 @@ func TestExecuteComparisonUsesOneExecutorAndInjectedApplication(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareRemainsAbsentFromRootDispatchAndHelp(t *testing.T) {
|
||||
runner := comparisonRunner(t, t.TempDir())
|
||||
var stdout, stderr bytes.Buffer
|
||||
if err := runner.Run(context.Background(), []string{"compare", "today"}, &stdout, &stderr); err == nil || err.Error() != `unknown command "compare"` {
|
||||
t.Fatalf("Run(compare) error = %v", err)
|
||||
func TestCompareCommandWritesStructuredPartialFailure(t *testing.T) {
|
||||
workingDir := t.TempDir()
|
||||
configPath := comparisonConfigPath(t, "weather_api:\n base_url: https://weather.api.example.com/\n")
|
||||
profileFailure := comparison.NewSafeError("generation", "execute prompt failed")
|
||||
result := comparisonResult("/reports/comparison-daily", []app.ComparisonProfileResult{
|
||||
{Position: 1, ProfileID: "weather-light", BackendID: "local", ModelName: "light", Status: comparison.StatusSucceeded, ValidationStatus: promptexec.ValidationPassed, ReportPath: "/reports/comparison-daily/01-weather-light.md", LLMDebugPath: "/debug/comparison-light"},
|
||||
{Position: 2, ProfileID: "weather-deep", BackendID: "cloud", ModelName: "deep", Status: comparison.StatusFailed, Error: &profileFailure},
|
||||
})
|
||||
partialErr := errors.New("comparison completed with 1 failed profiles")
|
||||
factoryCalls, applicationCalls := 0, 0
|
||||
runner := Runner{
|
||||
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 30, 0, 0, time.UTC)}, WorkingDir: workingDir,
|
||||
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
|
||||
factoryCalls++
|
||||
return &factoryExecutor{}, nil
|
||||
},
|
||||
compareDetailed: func(context.Context, app.ComparisonRequest) (*app.ComparisonResult, error) {
|
||||
applicationCalls++
|
||||
return result, partialErr
|
||||
},
|
||||
}
|
||||
if err := runner.Run(context.Background(), []string{"--help"}, &stdout, &stderr); err != nil || strings.Contains(stdout.String(), "compare") {
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runner.Run(context.Background(), []string{"compare", "daily", "--date", "2026-05-29", "--profile", "weather-light", "--profile", "weather-deep", "--config", configPath}, &stdout, &stderr)
|
||||
if !errors.Is(err, partialErr) || factoryCalls != 1 || applicationCalls != 1 || stderr.Len() != 0 {
|
||||
t.Fatalf("error/calls/stderr = %v/%d/%d/%q", err, factoryCalls, applicationCalls, stderr.String())
|
||||
}
|
||||
var summary comparisonSummary
|
||||
if err := json.Unmarshal(stdout.Bytes(), &summary); err != nil {
|
||||
t.Fatalf("decode summary: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if summary.Command != commandCompare || summary.Status != summaryStatusFailed || summary.Error == nil || summary.Error.Message != partialErr.Error() || summary.OutputDirectory != result.OutputDirectory || len(summary.Results) != 2 || summary.Results[0].ReportPath != result.Results[0].ReportPath || summary.Results[0].LLMDebugPath != result.Results[0].LLMDebugPath || summary.Results[1].Error == nil {
|
||||
t.Fatalf("summary = %#v", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareCommandWritesSuccessForDefaultAndExplicitDestinations(t *testing.T) {
|
||||
workingDir := t.TempDir()
|
||||
configPath := comparisonConfigPath(t, "weather_api:\n base_url: https://weather.api.example.com/\noutput:\n directory: configured-reports\n")
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
outputArgument []string
|
||||
outputDirectory string
|
||||
wantRequestPath string
|
||||
}{
|
||||
{name: "configured default", outputDirectory: filepath.Join(workingDir, "configured-reports", "comparison-daily-2026-05-29")},
|
||||
{name: "explicit directory", outputArgument: []string{"--out-dir", "published"}, outputDirectory: filepath.Join(workingDir, "published"), wantRequestPath: filepath.Join(workingDir, "published")},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var received app.ComparisonRequest
|
||||
runner := comparisonRunner(t, workingDir)
|
||||
runner.compareDetailed = func(_ context.Context, req app.ComparisonRequest) (*app.ComparisonResult, error) {
|
||||
received = req
|
||||
return comparisonResult(test.outputDirectory, []app.ComparisonProfileResult{
|
||||
{Position: 1, ProfileID: "weather-light", ModelName: "light", Status: comparison.StatusSucceeded, ValidationStatus: promptexec.ValidationPassed, ReportPath: filepath.Join(test.outputDirectory, "01-weather-light.md")},
|
||||
{Position: 2, ProfileID: "weather-deep", ModelName: "deep", Status: comparison.StatusSucceeded, ValidationStatus: promptexec.ValidationPassed, ReportPath: filepath.Join(test.outputDirectory, "02-weather-deep.md")},
|
||||
}), nil
|
||||
}
|
||||
args := []string{"compare", "daily", "--date", "2026-05-29", "--profile", "weather-light", "--profile", "weather-deep", "--config", configPath}
|
||||
args = append(args, test.outputArgument...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
if err := runner.Run(context.Background(), args, &stdout, &stderr); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
var summary comparisonSummary
|
||||
if err := json.Unmarshal(stdout.Bytes(), &summary); err != nil || summary.Status != summaryStatusSucceeded || summary.OutputDirectory != test.outputDirectory || stderr.Len() != 0 || received.OutputDir != test.wantRequestPath {
|
||||
t.Fatalf("summary/error/stderr/request = %#v/%v/%q/%#v", summary, err, stderr.String(), received)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareCommandQuietPreservesFailure(t *testing.T) {
|
||||
configPath := comparisonConfigPath(t, "weather_api:\n base_url: https://weather.api.example.com/\n")
|
||||
failure := errors.New("comparison completed with 2 failed profiles")
|
||||
calls := 0
|
||||
runner := comparisonRunner(t, t.TempDir())
|
||||
runner.compareDetailed = func(context.Context, app.ComparisonRequest) (*app.ComparisonResult, error) {
|
||||
calls++
|
||||
return comparisonResult("/reports/comparison-daily", nil), failure
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runner.Run(context.Background(), []string{"compare", "daily", "--date", "2026-05-29", "--profile", "weather-light", "--profile", "weather-deep", "--quiet", "--config", configPath}, &stdout, &stderr)
|
||||
if !errors.Is(err, failure) || calls != 1 || stdout.Len() != 0 || stderr.Len() != 0 {
|
||||
t.Fatalf("error/calls/stdout/stderr = %v/%d/%q/%q", err, calls, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareCommandLeavesPreExecutionFailuresUnstructured(t *testing.T) {
|
||||
runner := comparisonRunner(t, t.TempDir())
|
||||
called := false
|
||||
runner.compareDetailed = func(context.Context, app.ComparisonRequest) (*app.ComparisonResult, error) {
|
||||
called = true
|
||||
return nil, nil
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runner.Run(context.Background(), []string{"compare", "today", "--profile", "only-one"}, &stdout, &stderr)
|
||||
if err == nil || called || stdout.Len() != 0 || stderr.Len() != 0 {
|
||||
t.Fatalf("error/called/stdout/stderr = %v/%t/%q/%q", err, called, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareHelpIncludesCommand(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
if err := (Runner{}).Run(context.Background(), []string{"--help"}, &stdout, &stderr); err != nil || !strings.Contains(stdout.String(), "weatherreporter compare REPORT") || !strings.Contains(stdout.String(), "--profile PROFILE") {
|
||||
t.Fatalf("help/error = %q/%v", stdout.String(), err)
|
||||
}
|
||||
}
|
||||
@@ -202,3 +301,23 @@ func comparisonConfigPath(t *testing.T, contents string) string {
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func comparisonResult(outputDirectory string, results []app.ComparisonProfileResult) *app.ComparisonResult {
|
||||
started := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC)
|
||||
result := &app.ComparisonResult{
|
||||
ComparisonID: "comparison_run-123", ReportID: "daily", ReportName: "Daily Report",
|
||||
PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0", PromptHash: strings.Repeat("a", 64),
|
||||
StartedAt: started, FinishedAt: started.Add(time.Minute), Timezone: "America/Chicago",
|
||||
ValidPeriod: timeutil.Period{Start: started, End: started.Add(24 * time.Hour)}, OutputDirectory: outputDirectory,
|
||||
ManifestPath: filepath.Join(outputDirectory, "comparison.json"), DataPackagePath: filepath.Join(outputDirectory, "data-package.yml"),
|
||||
Results: append([]app.ComparisonProfileResult(nil), results...), Total: len(results),
|
||||
}
|
||||
for _, profile := range results {
|
||||
if profile.Status == "succeeded" {
|
||||
result.Succeeded++
|
||||
} else {
|
||||
result.Failed++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/comparison"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
@@ -12,6 +18,7 @@ import (
|
||||
const (
|
||||
commandGenerate = "generate"
|
||||
commandRun = "run"
|
||||
commandCompare = "compare"
|
||||
|
||||
summaryStatusSucceeded = "succeeded"
|
||||
summaryStatusFailed = "failed"
|
||||
@@ -67,6 +74,41 @@ type batchSummary struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type comparisonSummary struct {
|
||||
Command string `json:"command"`
|
||||
ComparisonID string `json:"comparisonId"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
ReportName string `json:"reportName"`
|
||||
PromptID string `json:"promptId"`
|
||||
PromptVersion string `json:"promptVersion"`
|
||||
PromptHash string `json:"promptHash"`
|
||||
Status string `json:"status"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
FinishedAt time.Time `json:"finishedAt"`
|
||||
Timezone string `json:"timezone"`
|
||||
ValidPeriod timeutil.Period `json:"validPeriod"`
|
||||
OutputDirectory string `json:"outputDirectory"`
|
||||
ManifestPath string `json:"manifestPath,omitempty"`
|
||||
DataPackagePath string `json:"dataPackagePath,omitempty"`
|
||||
Total int `json:"total"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Results []comparisonProfileSummary `json:"results"`
|
||||
Error *comparison.SafeError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type comparisonProfileSummary struct {
|
||||
Position int `json:"position"`
|
||||
ProfileID string `json:"profileId"`
|
||||
BackendID string `json:"backendId,omitempty"`
|
||||
ModelName string `json:"modelName"`
|
||||
Status string `json:"status"`
|
||||
ValidationStatus string `json:"validationStatus,omitempty"`
|
||||
ReportPath string `json:"reportPath,omitempty"`
|
||||
LLMDebugPath string `json:"llmDebugPath,omitempty"`
|
||||
Error *comparison.SafeError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func newGenerateSummary(result *app.ReportResult, err error) generateSummary {
|
||||
summary := generateSummary{Command: commandGenerate}
|
||||
if result == nil {
|
||||
@@ -139,6 +181,66 @@ func newBatchSummary(result *app.BatchResult) batchSummary {
|
||||
return summary
|
||||
}
|
||||
|
||||
func newComparisonSummary(result *app.ComparisonResult, err error) comparisonSummary {
|
||||
summary := comparisonSummary{Command: commandCompare, Results: []comparisonProfileSummary{}}
|
||||
if result == nil {
|
||||
return summary
|
||||
}
|
||||
summary.ComparisonID = result.ComparisonID
|
||||
summary.ReportID, summary.ReportName = result.ReportID, result.ReportName
|
||||
summary.PromptID, summary.PromptVersion, summary.PromptHash = result.PromptID, result.PromptVersion, result.PromptHash
|
||||
summary.StartedAt, summary.FinishedAt = result.StartedAt, result.FinishedAt
|
||||
summary.Timezone, summary.ValidPeriod = result.Timezone, result.ValidPeriod
|
||||
summary.OutputDirectory = result.OutputDirectory
|
||||
summary.ManifestPath, summary.DataPackagePath = result.ManifestPath, result.DataPackagePath
|
||||
summary.Total, summary.Succeeded, summary.Failed = result.Total, result.Succeeded, result.Failed
|
||||
for _, profile := range result.Results {
|
||||
summary.Results = append(summary.Results, comparisonProfileSummary{
|
||||
Position: profile.Position, ProfileID: profile.ProfileID, BackendID: profile.BackendID, ModelName: profile.ModelName,
|
||||
Status: profile.Status, ValidationStatus: string(profile.ValidationStatus), ReportPath: profile.ReportPath,
|
||||
LLMDebugPath: profile.LLMDebugPath, Error: profile.Error,
|
||||
})
|
||||
}
|
||||
summary.Status = comparisonSummaryStatus(result, err)
|
||||
if err != nil {
|
||||
summary.Error = safeComparisonSummaryError(err)
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
func comparisonSummaryStatus(result *app.ComparisonResult, err error) string {
|
||||
if result == nil || err != nil || result.Total < 2 || result.Succeeded != result.Total || result.Failed != 0 || result.ManifestPath == "" || result.DataPackagePath == "" {
|
||||
return summaryStatusFailed
|
||||
}
|
||||
return summaryStatusSucceeded
|
||||
}
|
||||
|
||||
func safeComparisonSummaryError(err error) *comparison.SafeError {
|
||||
message := "comparison did not complete"
|
||||
if aggregate, ok := comparisonAggregateErrorMessage(err.Error()); ok {
|
||||
message = aggregate
|
||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
||||
message = "comparison deadline exceeded"
|
||||
} else if errors.Is(err, context.Canceled) {
|
||||
message = "comparison canceled"
|
||||
}
|
||||
safe := comparison.NewSafeError("application", message)
|
||||
return &safe
|
||||
}
|
||||
|
||||
func comparisonAggregateErrorMessage(value string) (string, bool) {
|
||||
const prefix = "comparison completed with "
|
||||
const suffix = " failed profiles"
|
||||
if !strings.HasPrefix(value, prefix) || !strings.HasSuffix(value, suffix) {
|
||||
return "", false
|
||||
}
|
||||
count, err := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(value, prefix), suffix))
|
||||
if err != nil || count < 1 {
|
||||
return "", false
|
||||
}
|
||||
return fmt.Sprintf("comparison completed with %d failed profiles", count), true
|
||||
}
|
||||
|
||||
func batchSummaryStatus(result *app.BatchResult) string {
|
||||
if result == nil {
|
||||
return ""
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/comparison"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
@@ -29,3 +32,106 @@ func TestGenerateSummaryUsesActiveResultFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestComparisonSummaryUsesLockedOrderAndSafeFields(t *testing.T) {
|
||||
started := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC)
|
||||
profileFailure := comparison.NewSafeError("generation", "execute prompt failed")
|
||||
result := &app.ComparisonResult{
|
||||
ComparisonID: "comparison_run-123", ReportID: report.Daily, ReportName: "Daily Report",
|
||||
PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0", PromptHash: strings.Repeat("a", 64),
|
||||
StartedAt: started, FinishedAt: started.Add(time.Minute), Timezone: "America/Chicago",
|
||||
ValidPeriod: timeutil.Period{Start: started, End: started.Add(24 * time.Hour)}, OutputDirectory: "/reports/comparison-daily",
|
||||
ManifestPath: "/reports/comparison-daily/comparison.json", DataPackagePath: "/reports/comparison-daily/data-package.yml",
|
||||
Total: 2, Succeeded: 1, Failed: 1,
|
||||
Results: []app.ComparisonProfileResult{
|
||||
{Position: 1, ProfileID: "weather-light", BackendID: "local", ModelName: "light", Status: comparison.StatusSucceeded, ValidationStatus: promptexec.ValidationPassed, ReportPath: "/reports/comparison-daily/01-weather-light.md"},
|
||||
{Position: 2, ProfileID: "weather-deep", BackendID: "cloud", ModelName: "deep", Status: comparison.StatusFailed, Error: &profileFailure},
|
||||
},
|
||||
}
|
||||
summary := newComparisonSummary(result, errors.New("comparison completed with 1 failed profiles"))
|
||||
if summary.Command != commandCompare || summary.Status != summaryStatusFailed || summary.Error == nil || summary.Error.Message != "comparison completed with 1 failed profiles" || len(summary.Results) != 2 || summary.Results[0].ReportPath == "" || summary.Results[1].Error == nil {
|
||||
t.Fatalf("summary = %#v", summary)
|
||||
}
|
||||
data, err := json.Marshal(summary)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previous := -1
|
||||
for _, field := range []string{"command", "comparisonId", "reportId", "reportName", "promptId", "promptVersion", "promptHash", "status", "startedAt", "finishedAt", "timezone", "validPeriod", "outputDirectory", "manifestPath", "dataPackagePath", "total", "succeeded", "failed", "results", "error"} {
|
||||
position := strings.Index(string(data), `"`+field+`":`)
|
||||
if field == "error" {
|
||||
position = strings.LastIndex(string(data), `"`+field+`":`)
|
||||
}
|
||||
if position <= previous {
|
||||
t.Fatalf("field order for %q in %s", field, data)
|
||||
}
|
||||
previous = position
|
||||
}
|
||||
for _, unsafe := range []string{"provider response", "rawOutput", "renderedPrompt", "endpoint"} {
|
||||
if strings.Contains(string(data), unsafe) {
|
||||
t.Fatalf("summary includes unsafe content %q: %s", unsafe, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestComparisonSummaryOmitsUnpublishedArtifactsAndBoundsErrors(t *testing.T) {
|
||||
result := &app.ComparisonResult{ComparisonID: "comparison_run-123", ReportID: report.Daily, Results: []app.ComparisonProfileResult{{Position: 1, ProfileID: "weather-light", ModelName: "light", Status: comparison.StatusFailed}}}
|
||||
summary := newComparisonSummary(result, context.Canceled)
|
||||
if summary.Status != summaryStatusFailed || summary.Error == nil || summary.Error.Message != "comparison canceled" || summary.Results == nil {
|
||||
t.Fatalf("summary = %#v", summary)
|
||||
}
|
||||
data, err := json.Marshal(summary)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &fields); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, omitted := range []string{"manifestPath", "dataPackagePath"} {
|
||||
if _, exists := fields[omitted]; exists {
|
||||
t.Fatalf("summary includes unpublished %s: %s", omitted, data)
|
||||
}
|
||||
}
|
||||
publicationResult := &app.ComparisonResult{
|
||||
ComparisonID: "comparison_run-123", ReportID: report.Daily, OutputDirectory: "/reports/comparison-daily", Total: 2, Succeeded: 2,
|
||||
Results: []app.ComparisonProfileResult{
|
||||
{Position: 1, ProfileID: "weather-light", ModelName: "light", Status: comparison.StatusSucceeded},
|
||||
{Position: 2, ProfileID: "weather-deep", ModelName: "deep", Status: comparison.StatusSucceeded},
|
||||
},
|
||||
}
|
||||
unsafe := errors.New("comparison completed with 1 failed profiles; provider response contains sensitive material")
|
||||
publicationSummary := newComparisonSummary(publicationResult, unsafe)
|
||||
if publicationSummary.Status != summaryStatusFailed || publicationSummary.Error == nil || publicationSummary.Error.Message != "comparison did not complete" || strings.Contains(publicationSummary.Error.Message, "sensitive") || publicationSummary.ManifestPath != "" || publicationSummary.DataPackagePath != "" {
|
||||
safe := publicationSummary.Error
|
||||
t.Fatalf("safe error = %#v", safe)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComparisonSummaryClassifiesCompleteAndAllFailedResults(t *testing.T) {
|
||||
started := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC)
|
||||
failure := comparison.NewSafeError("generation", "execute prompt failed")
|
||||
complete := &app.ComparisonResult{
|
||||
ComparisonID: "comparison_run-123", ReportID: report.Daily, PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0", PromptHash: strings.Repeat("a", 64),
|
||||
StartedAt: started, FinishedAt: started, Timezone: "America/Chicago", ValidPeriod: timeutil.Period{Start: started, End: started.Add(time.Hour)},
|
||||
OutputDirectory: "/reports/comparison-daily", ManifestPath: "/reports/comparison-daily/comparison.json", DataPackagePath: "/reports/comparison-daily/data-package.yml",
|
||||
Total: 2, Succeeded: 2,
|
||||
Results: []app.ComparisonProfileResult{
|
||||
{Position: 1, ProfileID: "weather-light", ModelName: "light", Status: comparison.StatusSucceeded, ValidationStatus: promptexec.ValidationPassed, ReportPath: "/reports/comparison-daily/01-weather-light.md"},
|
||||
{Position: 2, ProfileID: "weather-deep", ModelName: "deep", Status: comparison.StatusSucceeded, ValidationStatus: promptexec.ValidationPassed, ReportPath: "/reports/comparison-daily/02-weather-deep.md"},
|
||||
},
|
||||
}
|
||||
if summary := newComparisonSummary(complete, nil); summary.Status != summaryStatusSucceeded || summary.Error != nil {
|
||||
t.Fatalf("complete summary = %#v", summary)
|
||||
}
|
||||
allFailed := *complete
|
||||
allFailed.Succeeded, allFailed.Failed = 0, 2
|
||||
allFailed.Results = []app.ComparisonProfileResult{
|
||||
{Position: 1, ProfileID: "weather-light", ModelName: "light", Status: comparison.StatusFailed, Error: &failure},
|
||||
{Position: 2, ProfileID: "weather-deep", ModelName: "deep", Status: comparison.StatusFailed, Error: &failure},
|
||||
}
|
||||
summary := newComparisonSummary(&allFailed, errors.New("comparison completed with 2 failed profiles"))
|
||||
if summary.Status != summaryStatusFailed || summary.Error == nil || summary.Error.Message != "comparison completed with 2 failed profiles" || summary.Results[0].ReportPath != "" || summary.Results[1].Error == nil {
|
||||
t.Fatalf("all-failed summary = %#v", summary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ Usage:
|
||||
weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--llm-debug-dir PATH] [--quiet]
|
||||
weatherreporter run morning [--config PATH] [--units VALUE] [--tz NAME] [--out-dir PATH] [--llm-debug-dir PATH] [--quiet]
|
||||
weatherreporter run evening [--config PATH] [--units VALUE] [--tz NAME] [--out-dir PATH] [--llm-debug-dir PATH] [--quiet]
|
||||
weatherreporter compare REPORT --profile PROFILE --profile PROFILE [--config PATH] [--units VALUE] [--tz NAME] [--date YYYY-MM-DD] [--out-dir PATH] [--replace] [--llm-debug-dir PATH] [--quiet]
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message.
|
||||
@@ -36,8 +37,10 @@ Options:
|
||||
--tz NAME Override weather API timezone.
|
||||
--out PATH Write the generated Markdown report to PATH.
|
||||
--llm-debug-dir PATH Write sensitive prompt debug artifacts under PATH.
|
||||
--out-dir PATH Write generated Markdown reports beneath PATH for run commands.
|
||||
--quiet Suppress successful generate and run output.
|
||||
--profile PROFILE Select a prompt profile for compare; repeat for every profile.
|
||||
--out-dir PATH Write generated Markdown reports beneath PATH for run commands, or select the exact comparison directory.
|
||||
--replace Authorize replacement of a recognized comparison bundle.
|
||||
--quiet Suppress successful action output.
|
||||
`
|
||||
|
||||
type Runner struct {
|
||||
@@ -109,6 +112,23 @@ func (r Runner) Run(ctx context.Context, args []string, stdout io.Writer, stderr
|
||||
}
|
||||
}
|
||||
return err
|
||||
case "compare":
|
||||
req, opts, err := r.resolveComparisonAction(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compareDetailed := r.compareDetailed
|
||||
if compareDetailed == nil {
|
||||
compareDetailed = app.CompareDetailed
|
||||
}
|
||||
result, err := compareDetailed(ctx, req)
|
||||
if result != nil {
|
||||
summary := newComparisonSummary(result, err)
|
||||
if encodeErr := writeActionResult(stdout, stderr, summary, outputOptions{Quiet: opts.Quiet}, nil); encodeErr != nil {
|
||||
return encodeErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("unknown command %q", args[0])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user