Validate generated text catalog before collection
This commit is contained in:
@@ -132,7 +132,7 @@ func compareDetailed(ctx context.Context, req ComparisonRequest, publish compari
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
prepared, err := prepareReport(prepareReportRequest{Config: req.Config, Resolved: resolved, Collection: *collection})
|
||||
prepared, err := prepareReport(prepareReportRequest{Config: req.Config, Resolved: resolved, Collection: *collection, handler: inspection.handler})
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("prepare comparison report: %w", err)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ type prepareReportRequest struct {
|
||||
Config config.Config
|
||||
Resolved report.Resolved
|
||||
Collection collect.Result
|
||||
handler generatedtext.Handler
|
||||
}
|
||||
|
||||
type preparationError struct {
|
||||
@@ -69,11 +70,6 @@ func prepareReport(req prepareReportRequest) (preparedReport, error) {
|
||||
if err != nil {
|
||||
return preparedReport{}, &preparationError{operation: "marshal data package", err: err}
|
||||
}
|
||||
handler, err := generatedtext.LookupDefinition(req.Resolved.Definition)
|
||||
if err != nil {
|
||||
return preparedReport{}, &preparationError{operation: "lookup generated text catalog", err: err}
|
||||
}
|
||||
|
||||
clonedDerived, err := clonePreparedValue(reportFacts.Derived)
|
||||
if err != nil {
|
||||
return preparedReport{}, &preparationError{operation: "copy prepared derived facts", err: err}
|
||||
@@ -93,7 +89,7 @@ func prepareReport(req prepareReportRequest) (preparedReport, error) {
|
||||
identity: clonedIdentity,
|
||||
sourceWarnings: append([]weatherdata.SourceWarning(nil), clonedIdentity.SourceWarnings...),
|
||||
dataPackage: append([]byte(nil), serializedDataPackage...),
|
||||
handler: handler,
|
||||
handler: req.handler,
|
||||
}
|
||||
return prepared, nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/collect"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/generatedtext"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
@@ -22,7 +24,7 @@ func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
|
||||
request := prepareReportRequest{Config: cfg, Resolved: resolved, Collection: collect.Result{Bundle: &bundle}}
|
||||
request := prepareReportRequest{Config: cfg, Resolved: resolved, Collection: collect.Result{Bundle: &bundle}, handler: preparedHandler(t, resolved)}
|
||||
prepared, err := prepareReport(request)
|
||||
if err != nil {
|
||||
t.Fatalf("prepareReport() error = %v", err)
|
||||
@@ -73,7 +75,7 @@ func TestPrepareReportProjectsPreparedIdentity(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
prepared, err := prepareReport(prepareReportRequest{Config: cfg, Resolved: resolved, Collection: collect.Result{Bundle: &bundle}})
|
||||
prepared, err := prepareReport(prepareReportRequest{Config: cfg, Resolved: resolved, Collection: collect.Result{Bundle: &bundle}, handler: preparedHandler(t, resolved)})
|
||||
if err != nil {
|
||||
t.Fatalf("prepareReport() error = %v", err)
|
||||
}
|
||||
@@ -108,3 +110,12 @@ func TestPrepareReportProjectsPreparedIdentity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func preparedHandler(t *testing.T, resolved report.Resolved) generatedtext.Handler {
|
||||
t.Helper()
|
||||
handler, err := generatedtext.LookupDefinition(resolved.Definition)
|
||||
if err != nil {
|
||||
t.Fatalf("LookupDefinition() error = %v", err)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func preparedDailyProfile(t *testing.T) (preparedReport, PromptInspectionResult)
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
bundle := generationBundle(t)
|
||||
prepared, err := prepareReport(prepareReportRequest{Config: cfg, Resolved: resolved, Collection: collect.Result{Bundle: &bundle}})
|
||||
prepared, err := prepareReport(prepareReportRequest{Config: cfg, Resolved: resolved, Collection: collect.Result{Bundle: &bundle}, handler: preparedHandler(t, resolved)})
|
||||
if err != nil {
|
||||
t.Fatalf("prepareReport() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
if result == nil {
|
||||
result = initialReportResult(req.GenerateRequest, req.Resolved, req.Inspection)
|
||||
}
|
||||
prepared, err := prepareReport(prepareReportRequest{Config: req.Config, Resolved: req.Resolved, Collection: req.Collection})
|
||||
prepared, err := prepareReport(prepareReportRequest{Config: req.Config, Resolved: req.Resolved, Collection: req.Collection, handler: req.Inspection.handler})
|
||||
if err != nil {
|
||||
return result, generatedPreparationError(req.Resolved, result.RunID, err)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/comparison"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/generatedtext"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
)
|
||||
@@ -29,6 +30,7 @@ type PromptInspectionResult struct {
|
||||
ProfileID string
|
||||
BackendID string
|
||||
ModelName string
|
||||
handler generatedtext.Handler
|
||||
}
|
||||
|
||||
// PromptExecutionsInspectionRequest validates all prompt/profile combinations
|
||||
@@ -56,6 +58,7 @@ type ComparisonInspectionResult struct {
|
||||
PromptVersion string
|
||||
PromptHash string
|
||||
Profiles []ComparisonProfileInspection
|
||||
handler generatedtext.Handler
|
||||
}
|
||||
|
||||
// ComparisonProfileInspection contains one requested profile's safe effective
|
||||
@@ -91,6 +94,10 @@ func InspectPromptExecutions(ctx context.Context, req PromptExecutionsInspection
|
||||
profiles := map[string]promptexec.ProfileInspection{}
|
||||
for _, resolved := range req.Resolved {
|
||||
definition := resolved.Definition
|
||||
handler, err := generatedtext.LookupDefinition(definition)
|
||||
if err != nil {
|
||||
return nil, promptexec.NewError(promptexec.InvalidConfiguration, "report generated-text catalog is incompatible", err)
|
||||
}
|
||||
inspection, err := inspectPromptContract(ctx, req.Executor, definition)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -113,6 +120,7 @@ func InspectPromptExecutions(ctx context.Context, req PromptExecutionsInspection
|
||||
results[definition.ID] = PromptInspectionResult{
|
||||
PromptID: inspection.PromptID, PromptVersion: inspection.PromptVersion, PromptHash: inspection.PromptHash,
|
||||
ProfileID: profile.ProfileID, BackendID: profile.BackendID, ModelName: profile.ModelName,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
@@ -130,6 +138,10 @@ func InspectComparisonExecution(ctx context.Context, req ComparisonInspectionReq
|
||||
return ComparisonInspectionResult{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt executor is required", nil)
|
||||
}
|
||||
|
||||
handler, err := generatedtext.LookupDefinition(req.Resolved.Definition)
|
||||
if err != nil {
|
||||
return ComparisonInspectionResult{}, comparisonInspectionError("comparison generated-text catalog inspection failed", promptexec.NewError(promptexec.InvalidConfiguration, "report generated-text catalog is incompatible", err))
|
||||
}
|
||||
inspection, err := inspectPromptContract(ctx, req.Executor, req.Resolved.Definition)
|
||||
if err != nil {
|
||||
return ComparisonInspectionResult{}, comparisonInspectionError("comparison prompt inspection failed", err)
|
||||
@@ -139,6 +151,7 @@ func InspectComparisonExecution(ctx context.Context, req ComparisonInspectionReq
|
||||
PromptVersion: inspection.PromptVersion,
|
||||
PromptHash: inspection.PromptHash,
|
||||
Profiles: make([]ComparisonProfileInspection, 0, len(req.ProfileIDs)),
|
||||
handler: handler,
|
||||
}
|
||||
for _, profileID := range req.ProfileIDs {
|
||||
profile, err := inspectPromptProfile(ctx, req.Executor, profileID, req.LookupEnv)
|
||||
|
||||
@@ -110,9 +110,7 @@ func TestInspectPromptExecutionReturnsSafeInspectionError(t *testing.T) {
|
||||
|
||||
func TestInspectPromptExecutionsReusesEffectiveProfile(t *testing.T) {
|
||||
first := inspectionResolved(t)
|
||||
second := first
|
||||
second.Definition.ID = report.Today
|
||||
second.Definition.PromptID = "weather.today"
|
||||
second := inspectionResolvedFor(t, report.Today)
|
||||
executor := &inspectionExecutor{
|
||||
prompt: validPromptInspection(first.Definition),
|
||||
profiles: map[string]promptexec.ProfileInspection{
|
||||
@@ -132,6 +130,67 @@ func TestInspectPromptExecutionsReusesEffectiveProfile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptInspectionRejectsIncompatibleGeneratedTextCatalogBeforeExecutorWork(t *testing.T) {
|
||||
base := inspectionResolved(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
resolved report.Resolved
|
||||
inspect func(context.Context, report.Resolved, *inspectionExecutor) error
|
||||
}{
|
||||
{
|
||||
name: "single report unknown template",
|
||||
resolved: func() report.Resolved {
|
||||
resolved := base
|
||||
resolved.Definition.TemplateID = "unknown"
|
||||
return resolved
|
||||
}(),
|
||||
inspect: func(ctx context.Context, resolved report.Resolved, executor *inspectionExecutor) error {
|
||||
_, err := InspectPromptExecution(ctx, PromptInspectionRequest{Resolved: resolved, Executor: executor})
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "batch known pair for another report",
|
||||
resolved: func() report.Resolved {
|
||||
resolved := base
|
||||
resolved.Definition.GeneratedTextSchemaID = "today"
|
||||
resolved.Definition.TemplateID = "today"
|
||||
return resolved
|
||||
}(),
|
||||
inspect: func(ctx context.Context, resolved report.Resolved, executor *inspectionExecutor) error {
|
||||
_, err := InspectPromptExecutions(ctx, PromptExecutionsInspectionRequest{Resolved: []report.Resolved{resolved}, Executor: executor})
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comparison known pair for another report",
|
||||
resolved: func() report.Resolved {
|
||||
resolved := base
|
||||
resolved.Definition.GeneratedTextSchemaID = "today"
|
||||
resolved.Definition.TemplateID = "today"
|
||||
return resolved
|
||||
}(),
|
||||
inspect: func(ctx context.Context, resolved report.Resolved, executor *inspectionExecutor) error {
|
||||
_, err := InspectComparisonExecution(ctx, ComparisonInspectionRequest{Resolved: resolved, ProfileIDs: []string{"weather-light", "weather-deep"}, Executor: executor})
|
||||
return err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
executor := &inspectionExecutor{}
|
||||
err := test.inspect(context.Background(), test.resolved, executor)
|
||||
if err == nil || promptexec.CategoryOf(err) != promptexec.InvalidConfiguration {
|
||||
t.Fatalf("inspection error/category = %v/%q, want invalid configuration", err, promptexec.CategoryOf(err))
|
||||
}
|
||||
if len(executor.promptRequests) != 0 || len(executor.profileRequests) != 0 || executor.executeRequests != 0 {
|
||||
t.Fatalf("incompatible catalog performed executor work: prompts %#v profiles %#v executions %d", executor.promptRequests, executor.profileRequests, executor.executeRequests)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectComparisonExecutionPreservesOrderedExplicitProfiles(t *testing.T) {
|
||||
resolved := inspectionResolved(t)
|
||||
executor := &inspectionExecutor{
|
||||
@@ -265,12 +324,16 @@ func (e *inspectionExecutor) Execute(context.Context, promptexec.ExecuteRequest,
|
||||
}
|
||||
|
||||
func inspectionResolved(t *testing.T) report.Resolved {
|
||||
return inspectionResolvedFor(t, report.Daily)
|
||||
}
|
||||
|
||||
func inspectionResolvedFor(t *testing.T, id report.ID) report.Resolved {
|
||||
t.Helper()
|
||||
resolved, err := report.DefaultRegistry().Resolve(report.Daily, report.ResolveRequest{
|
||||
Now: time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC),
|
||||
Date: time.Date(2026, 5, 29, 0, 0, 0, 0, time.UTC),
|
||||
Location: time.UTC,
|
||||
})
|
||||
request := report.ResolveRequest{Now: time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC), Location: time.UTC}
|
||||
if id == report.Daily {
|
||||
request.Date = time.Date(2026, 5, 29, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
resolved, err := report.DefaultRegistry().Resolve(id, request)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user