Run report generation without workspace state
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
@@ -366,21 +367,40 @@ func preparationArtifactFor(resolved report.Resolved, paths ArtifactPaths) Promp
|
||||
artifact.DataPackagePath = paths.DataPackage
|
||||
artifact.Preparation.PromptID, artifact.Preparation.PromptVersion = artifact.PromptID, artifact.PromptVersion
|
||||
artifact.Preparation.PromptHash = "prompt-hash"
|
||||
artifact.Preparation.DataPackagePath = paths.DataPackage
|
||||
return artifact
|
||||
}
|
||||
|
||||
func validPreparationArtifact() PromptPreparationArtifact {
|
||||
started := time.Date(2026, 5, 29, 15, 0, 0, 0, time.UTC)
|
||||
return PromptPreparationArtifact{
|
||||
SchemaVersion: PromptPreparationSchemaVersion, Status: PromptPreparationSucceeded,
|
||||
ReportID: report.Daily, RunID: "weatherreporter-run", PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0",
|
||||
DataPackagePath: "/workspace/data.yaml", Preparation: &promptexec.Preparation{PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0"},
|
||||
StartedAt: started, EndedAt: started.Add(time.Second), Duration: time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func executionArtifactFor(resolved report.Resolved, paths ArtifactPaths) PromptExecutionArtifact {
|
||||
artifact := validExecutionArtifact()
|
||||
metadata := resolved.Metadata()
|
||||
artifact.ReportID, artifact.RunID = metadata.ReportID, metadata.RunID
|
||||
artifact.PromptID, artifact.PromptVersion = resolved.Definition.PromptID, resolved.Definition.PromptVersion
|
||||
artifact.Provenance.PromptID, artifact.Provenance.PromptVersion = artifact.PromptID, artifact.PromptVersion
|
||||
artifact.Provenance.DataPackagePath = paths.DataPackage
|
||||
artifact.Paths.RawOutputPath = paths.GeneratedTextRaw
|
||||
return artifact
|
||||
}
|
||||
|
||||
func validExecutionArtifact() PromptExecutionArtifact {
|
||||
started := time.Date(2026, 5, 29, 15, 0, 0, 0, time.UTC)
|
||||
validation := promptexec.NewValidation(promptexec.ValidationPassed, "json_schema", "daily.generated_text.schema.json", nil)
|
||||
return PromptExecutionArtifact{
|
||||
SchemaVersion: PromptExecutionSchemaVersion, Status: PromptExecutionSucceeded,
|
||||
ReportID: report.Daily, RunID: "weatherreporter-run", PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0",
|
||||
Provenance: &PromptExecutionProvenance{RunID: "provider-run", PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0", PromptHash: "prompt-hash", RenderedPromptHash: "rendered-hash", ProfileID: "profile", BackendID: "backend", ModelName: "model", StartedAt: started, EndedAt: started.Add(time.Second), Duration: time.Second},
|
||||
Validation: &validation, StartedAt: started, EndedAt: started.Add(time.Second), Duration: time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSONFixture(t *testing.T, path string, value any) {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(value)
|
||||
|
||||
@@ -81,7 +81,7 @@ func (a PromptPreparationArtifact) Validate() error {
|
||||
if a.Preparation == nil || a.Error != nil {
|
||||
return fmt.Errorf("successful prompt preparation requires preparation without an error")
|
||||
}
|
||||
if a.Preparation.PromptID != a.PromptID || a.Preparation.PromptVersion != a.PromptVersion || a.Preparation.DataPackagePath != a.DataPackagePath {
|
||||
if a.Preparation.PromptID != a.PromptID || a.Preparation.PromptVersion != a.PromptVersion {
|
||||
return fmt.Errorf("successful prompt preparation provenance must match the artifact")
|
||||
}
|
||||
case PromptPreparationFailed:
|
||||
@@ -122,7 +122,6 @@ type PromptExecutionProvenance struct {
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
EndedAt time.Time `json:"endedAt"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
DataPackagePath string `json:"dataPackagePath"`
|
||||
}
|
||||
|
||||
// PromptExecutionPaths records only destinations reached by a completed run.
|
||||
@@ -165,7 +164,6 @@ func PromptExecutionProvenanceFrom(value promptexec.Execution) PromptExecutionPr
|
||||
InputHashes: inputHashes, ProfileID: value.ProfileID, BackendID: value.BackendID,
|
||||
ModelName: value.ModelName, GeneratedHash: value.GeneratedHash, Usage: value.Usage,
|
||||
StartedAt: value.StartedAt, EndedAt: value.EndedAt, Duration: value.Duration,
|
||||
DataPackagePath: value.DataPackagePath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +253,6 @@ func validatePromptExecutionProvenance(artifact PromptExecutionArtifact) error {
|
||||
}{
|
||||
{"run id", value.RunID}, {"prompt hash", value.PromptHash}, {"rendered prompt hash", value.RenderedPromptHash},
|
||||
{"profile id", value.ProfileID}, {"backend id", value.BackendID}, {"model name", value.ModelName},
|
||||
{"data package path", value.DataPackagePath},
|
||||
} {
|
||||
if strings.TrimSpace(required.value) == "" {
|
||||
return fmt.Errorf("completed prompt execution provenance %s is required", required.name)
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
)
|
||||
|
||||
func TestPromptPreparationArtifactValidation(t *testing.T) {
|
||||
valid := validPreparationArtifact()
|
||||
if err := valid.Validate(); err != nil {
|
||||
t.Fatalf("valid successful preparation: %v", err)
|
||||
}
|
||||
failed := valid
|
||||
failed.Status = PromptPreparationFailed
|
||||
failed.Preparation = nil
|
||||
failed.Error = &PromptArtifactError{Category: promptexec.Generation, Message: "provider unavailable"}
|
||||
if err := failed.Validate(); err != nil {
|
||||
t.Fatalf("valid failed preparation: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*PromptPreparationArtifact)
|
||||
}{
|
||||
{"report id", func(a *PromptPreparationArtifact) { a.ReportID = "" }},
|
||||
{"run id", func(a *PromptPreparationArtifact) { a.RunID = "" }},
|
||||
{"prompt id", func(a *PromptPreparationArtifact) { a.PromptID = "" }},
|
||||
{"prompt id for another report", func(a *PromptPreparationArtifact) { a.PromptID = "weather.hourly_generated_text" }},
|
||||
{"prompt version", func(a *PromptPreparationArtifact) { a.PromptVersion = "latest" }},
|
||||
{"data package", func(a *PromptPreparationArtifact) { a.DataPackagePath = "" }},
|
||||
{"start time", func(a *PromptPreparationArtifact) { a.StartedAt = time.Time{} }},
|
||||
{"end time", func(a *PromptPreparationArtifact) { a.EndedAt = time.Time{} }},
|
||||
{"negative duration", func(a *PromptPreparationArtifact) { a.Duration = -time.Second }},
|
||||
{"reversed times", func(a *PromptPreparationArtifact) { a.EndedAt = a.StartedAt.Add(-time.Second) }},
|
||||
{"missing provenance", func(a *PromptPreparationArtifact) { a.Preparation = nil }},
|
||||
{"success error", func(a *PromptPreparationArtifact) {
|
||||
a.Error = &PromptArtifactError{Category: promptexec.Generation, Message: "failed"}
|
||||
}},
|
||||
{"provenance prompt id", func(a *PromptPreparationArtifact) { a.Preparation.PromptID = "other" }},
|
||||
{"provenance prompt version", func(a *PromptPreparationArtifact) { a.Preparation.PromptVersion = "other" }},
|
||||
{"provenance data package", func(a *PromptPreparationArtifact) { a.Preparation.DataPackagePath = "/other/data.yaml" }},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
artifact := validPreparationArtifact()
|
||||
test.mutate(&artifact)
|
||||
if err := artifact.Validate(); err == nil {
|
||||
t.Fatalf("Validate() error = nil for %#v", artifact)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailedPromptPreparationRejectsContradictoryDetails(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*PromptPreparationArtifact)
|
||||
}{
|
||||
{"missing error", func(a *PromptPreparationArtifact) { a.Error = nil }},
|
||||
{"unknown category", func(a *PromptPreparationArtifact) { a.Error.Category = promptexec.ErrorCategory("other") }},
|
||||
{"empty message", func(a *PromptPreparationArtifact) { a.Error.Message = " " }},
|
||||
{"oversized message", func(a *PromptPreparationArtifact) { a.Error.Message = strings.Repeat("x", promptArtifactErrorLimit+1) }},
|
||||
{"invented provenance", func(a *PromptPreparationArtifact) { a.Preparation = validPreparationArtifact().Preparation }},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
artifact := validPreparationArtifact()
|
||||
artifact.Status = PromptPreparationFailed
|
||||
artifact.Preparation = nil
|
||||
artifact.Error = &PromptArtifactError{Category: promptexec.Generation, Message: "provider unavailable"}
|
||||
test.mutate(&artifact)
|
||||
if err := artifact.Validate(); err == nil {
|
||||
t.Fatalf("Validate() error = nil for %#v", artifact)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptExecutionArtifactValidation(t *testing.T) {
|
||||
valid := validExecutionArtifact()
|
||||
valid.Provenance.RunID = "provider-run-different-from-weatherreporter"
|
||||
valid.Provenance.GeneratedHash = ""
|
||||
valid.Provenance.Usage = promptexec.TokenUsage{}
|
||||
if err := valid.Validate(); err != nil {
|
||||
t.Fatalf("valid completed execution with provider run identity and omitted counters: %v", err)
|
||||
}
|
||||
rejected := validExecutionArtifact()
|
||||
rejected.Status = PromptExecutionValidationRejected
|
||||
validation := promptexec.NewValidation(promptexec.ValidationFailed, "json_schema", "daily.generated_text.schema.json", []string{"schema mismatch"})
|
||||
rejected.Validation = &validation
|
||||
if err := rejected.Validate(); err != nil {
|
||||
t.Fatalf("valid validation rejection: %v", err)
|
||||
}
|
||||
failed := validFailedExecutionArtifact()
|
||||
if err := failed.Validate(); err != nil {
|
||||
t.Fatalf("valid operational failure: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*PromptExecutionArtifact)
|
||||
}{
|
||||
{"report id", func(a *PromptExecutionArtifact) { a.ReportID = "" }},
|
||||
{"run id", func(a *PromptExecutionArtifact) { a.RunID = "" }},
|
||||
{"prompt id", func(a *PromptExecutionArtifact) { a.PromptID = "" }},
|
||||
{"prompt version", func(a *PromptExecutionArtifact) { a.PromptVersion = "latest" }},
|
||||
{"start time", func(a *PromptExecutionArtifact) { a.StartedAt = time.Time{} }},
|
||||
{"end time", func(a *PromptExecutionArtifact) { a.EndedAt = time.Time{} }},
|
||||
{"negative duration", func(a *PromptExecutionArtifact) { a.Duration = -time.Second }},
|
||||
{"reversed times", func(a *PromptExecutionArtifact) { a.EndedAt = a.StartedAt.Add(-time.Second) }},
|
||||
{"missing provenance", func(a *PromptExecutionArtifact) { a.Provenance = nil }},
|
||||
{"missing validation", func(a *PromptExecutionArtifact) { a.Validation = nil }},
|
||||
{"wrong validation", func(a *PromptExecutionArtifact) {
|
||||
value := promptexec.NewValidation(promptexec.ValidationFailed, "json_schema", "schema.json", nil)
|
||||
a.Validation = &value
|
||||
}},
|
||||
{"operational error", func(a *PromptExecutionArtifact) {
|
||||
a.Error = &PromptArtifactError{Category: promptexec.Generation, Message: "failed"}
|
||||
}},
|
||||
{"provenance prompt id", func(a *PromptExecutionArtifact) { a.Provenance.PromptID = "other" }},
|
||||
{"provenance prompt version", func(a *PromptExecutionArtifact) { a.Provenance.PromptVersion = "other" }},
|
||||
{"provenance run id", func(a *PromptExecutionArtifact) { a.Provenance.RunID = "" }},
|
||||
{"prompt hash", func(a *PromptExecutionArtifact) { a.Provenance.PromptHash = "" }},
|
||||
{"rendered hash", func(a *PromptExecutionArtifact) { a.Provenance.RenderedPromptHash = "" }},
|
||||
{"profile id", func(a *PromptExecutionArtifact) { a.Provenance.ProfileID = "" }},
|
||||
{"backend id", func(a *PromptExecutionArtifact) { a.Provenance.BackendID = "" }},
|
||||
{"model name", func(a *PromptExecutionArtifact) { a.Provenance.ModelName = "" }},
|
||||
{"data package", func(a *PromptExecutionArtifact) { a.Provenance.DataPackagePath = "" }},
|
||||
{"provenance start time", func(a *PromptExecutionArtifact) { a.Provenance.StartedAt = time.Time{} }},
|
||||
{"provenance end time", func(a *PromptExecutionArtifact) { a.Provenance.EndedAt = time.Time{} }},
|
||||
{"provenance negative duration", func(a *PromptExecutionArtifact) { a.Provenance.Duration = -time.Second }},
|
||||
{"provenance reversed times", func(a *PromptExecutionArtifact) { a.Provenance.EndedAt = a.Provenance.StartedAt.Add(-time.Second) }},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
artifact := validExecutionArtifact()
|
||||
test.mutate(&artifact)
|
||||
if err := artifact.Validate(); err == nil {
|
||||
t.Fatalf("Validate() error = nil for %#v", artifact)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailedPromptExecutionRejectsContradictoryDetails(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*PromptExecutionArtifact)
|
||||
}{
|
||||
{"missing error", func(a *PromptExecutionArtifact) { a.Error = nil }},
|
||||
{"unknown category", func(a *PromptExecutionArtifact) { a.Error.Category = promptexec.ErrorCategory("other") }},
|
||||
{"provenance", func(a *PromptExecutionArtifact) { a.Provenance = validExecutionArtifact().Provenance }},
|
||||
{"completed validation", func(a *PromptExecutionArtifact) { a.Validation = validExecutionArtifact().Validation }},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
artifact := validFailedExecutionArtifact()
|
||||
test.mutate(&artifact)
|
||||
if err := artifact.Validate(); err == nil {
|
||||
t.Fatalf("Validate() error = nil for %#v", artifact)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func validPreparationArtifact() PromptPreparationArtifact {
|
||||
started := time.Date(2026, 5, 29, 15, 0, 0, 0, time.UTC)
|
||||
return PromptPreparationArtifact{
|
||||
SchemaVersion: PromptPreparationSchemaVersion, Status: PromptPreparationSucceeded,
|
||||
ReportID: report.Daily, RunID: "weatherreporter-run", PromptID: "weather.daily_generated_text",
|
||||
PromptVersion: "2.0.0", DataPackagePath: "/workspace/data.yaml",
|
||||
Preparation: &promptexec.Preparation{
|
||||
PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0",
|
||||
DataPackagePath: "/workspace/data.yaml",
|
||||
},
|
||||
StartedAt: started, EndedAt: started.Add(time.Second), Duration: time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func validExecutionArtifact() PromptExecutionArtifact {
|
||||
started := time.Date(2026, 5, 29, 15, 0, 0, 0, time.UTC)
|
||||
validation := promptexec.NewValidation(promptexec.ValidationPassed, "json_schema", "daily.generated_text.schema.json", nil)
|
||||
return PromptExecutionArtifact{
|
||||
SchemaVersion: PromptExecutionSchemaVersion, Status: PromptExecutionSucceeded,
|
||||
ReportID: report.Daily, RunID: "weatherreporter-run", PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0",
|
||||
Provenance: &PromptExecutionProvenance{
|
||||
RunID: "provider-run", PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0",
|
||||
PromptHash: "prompt-hash", RenderedPromptHash: "rendered-hash", ProfileID: "profile",
|
||||
BackendID: "backend", ModelName: "model", DataPackagePath: "/workspace/data.yaml",
|
||||
StartedAt: started, EndedAt: started.Add(time.Second), Duration: time.Second,
|
||||
},
|
||||
Validation: &validation, StartedAt: started, EndedAt: started.Add(time.Second), Duration: time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func validFailedExecutionArtifact() PromptExecutionArtifact {
|
||||
started := time.Date(2026, 5, 29, 15, 0, 0, 0, time.UTC)
|
||||
return PromptExecutionArtifact{
|
||||
SchemaVersion: PromptExecutionSchemaVersion, Status: PromptExecutionFailed,
|
||||
ReportID: report.Daily, RunID: "weatherreporter-run", PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0",
|
||||
StartedAt: started, EndedAt: started, Error: &PromptArtifactError{Category: promptexec.Generation, Message: "provider unavailable"},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user