Add durable prompt execution state records

This commit is contained in:
2026-07-31 04:13:00 +00:00
parent b0b703eab4
commit a52a6ed22a
9 changed files with 619 additions and 65 deletions

View File

@@ -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"
@@ -30,6 +31,8 @@ func TestPathsUseRunIDAndWorkspace(t *testing.T) {
filepath.Join("snapshots", "daily", "2026-05-29", "modules.20260529T100000.000000000Z_daily_2026-05-29.json"),
filepath.Join("snapshots", "daily", "2026-05-29", "metadata.20260529T100000.000000000Z_daily_2026-05-29.json"),
filepath.Join("data-packages", "daily", "2026-05-29", "data_package.20260529T100000.000000000Z_daily_2026-05-29.yaml"),
filepath.Join("preflight", "daily", "2026-05-29", "prompt_preparation.20260529T100000.000000000Z_daily_2026-05-29.json"),
filepath.Join("snapshots", "daily", "2026-05-29", "prompt_execution.20260529T100000.000000000Z_daily_2026-05-29.json"),
filepath.Join("preflight", "daily", "2026-05-29", "render.20260529T100000.000000000Z_daily_2026-05-29.json"),
filepath.Join("notifications", "daily", "2026-05-29", "distributor.20260529T100000.000000000Z_daily_2026-05-29.json"),
filepath.Join("reports", "daily", "2026-05-29", "report.20260529T100000.000000000Z_daily_2026-05-29.md"),
@@ -40,6 +43,146 @@ func TestPathsUseRunIDAndWorkspace(t *testing.T) {
}
}
func TestPromptArtifactsAndV2MetadataRoundTrip(t *testing.T) {
store := newTestStore(t)
resolved := resolveDailyAt(t, "2026-05-29T05:00:00-05:00")
paths, err := store.Paths(resolved)
if err != nil {
t.Fatalf("Paths() error = %v", err)
}
metadata := resolved.Metadata()
preparationPath, err := store.SavePromptPreparation(context.Background(), resolved, PromptPreparationArtifact{
Status: PromptPreparationSucceeded,
ReportID: metadata.ReportID,
RunID: metadata.RunID,
PromptID: metadata.PromptID,
PromptVersion: "v1",
DataPackagePath: paths.DataPackage,
Preparation: &promptexec.Preparation{
PromptID: metadata.PromptID, PromptVersion: "v1", PromptHash: "prompt-hash",
DataPackagePath: paths.DataPackage,
},
})
if err != nil {
t.Fatalf("SavePromptPreparation() error = %v", err)
}
executionPath, err := store.SavePromptExecution(context.Background(), resolved, PromptExecutionArtifact{
Status: PromptExecutionSucceeded,
ReportID: metadata.ReportID,
RunID: metadata.RunID,
PromptID: metadata.PromptID,
PromptVersion: "v1",
Provenance: &PromptExecutionProvenance{
RunID: metadata.RunID, PromptID: metadata.PromptID, PromptVersion: "v1",
PromptHash: "prompt-hash", DataPackagePath: paths.DataPackage,
},
Validation: func() *promptexec.Validation {
value := promptexec.NewValidation(promptexec.ValidationPassed, "json_schema", "schemas/report.json", nil)
return &value
}(),
Paths: PromptExecutionPaths{RawOutputPath: paths.GeneratedTextRaw, RenderedReportPath: paths.RenderedReport},
})
if err != nil {
t.Fatalf("SavePromptExecution() error = %v", err)
}
loadedPreparation, err := store.LoadPromptPreparation(context.Background(), preparationPath)
if err != nil || loadedPreparation.Preparation == nil || loadedPreparation.Preparation.PromptHash != "prompt-hash" {
t.Fatalf("LoadPromptPreparation() = %#v, %v", loadedPreparation, err)
}
loadedExecution, err := store.LoadPromptExecution(context.Background(), executionPath)
if err != nil || loadedExecution.Provenance == nil || loadedExecution.Validation == nil {
t.Fatalf("LoadPromptExecution() = %#v, %v", loadedExecution, err)
}
executionData, err := os.ReadFile(executionPath)
if err != nil {
t.Fatalf("read execution artifact: %v", err)
}
if strings.Contains(string(executionData), `"RawOutput"`) || strings.Contains(string(executionData), `"Debug"`) {
t.Fatalf("execution artifact contains sensitive content fields: %s", executionData)
}
v2 := BuildPromptMetadataFromBriefingMetadata(resolved, stateBriefingMetadata(resolved), paths)
v2.PreparationPath = preparationPath
v2.ExecutionPath = executionPath
metadataPath, err := store.SaveMetadata(context.Background(), v2)
if err != nil {
t.Fatalf("SaveMetadata() error = %v", err)
}
data, err := os.ReadFile(metadataPath)
if err != nil {
t.Fatalf("read v2 metadata: %v", err)
}
if strings.Contains(string(data), "preflightPath") || strings.Contains(string(data), "generatedTextResultPath") {
t.Fatalf("v2 metadata contains deprecated aliases: %s", data)
}
loadedMetadata, _, err := store.LoadMetadataByRunID(context.Background(), metadata.RunID)
if err != nil || loadedMetadata.PreparationPath != preparationPath || loadedMetadata.ExecutionPath != executionPath {
t.Fatalf("LoadMetadataByRunID() = %#v, %v", loadedMetadata, err)
}
}
func TestMetadataV1CompatibilityAndUnknownVersion(t *testing.T) {
legacy := Metadata{
SchemaVersion: MetadataSchemaVersionV1,
RunID: "legacy-run",
MetadataPath: "/tmp/metadata.legacy-run.json",
ReportID: report.Daily,
PromptID: "weather.daily",
ModuleSnapshotPath: "/tmp/modules.json",
DataPackagePath: "/tmp/data.yaml",
PreflightPath: "/tmp/render.json",
GeneratedTextResultPath: "/tmp/generated-result.json",
}
data, err := json.Marshal(legacy)
if err != nil {
t.Fatalf("Marshal() error = %v", err)
}
if !strings.Contains(string(data), "preflightPath") || !strings.Contains(string(data), "generatedTextResultPath") || strings.Contains(string(data), "preparationPath") || strings.Contains(string(data), "executionPath") {
t.Fatalf("legacy metadata JSON = %s", data)
}
var decoded Metadata
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if decoded.PreparationPath != legacy.PreflightPath || decoded.ExecutionPath != legacy.GeneratedTextResultPath {
t.Fatalf("decoded compatibility paths = %#v", decoded)
}
remarshaled, err := json.Marshal(decoded)
if err != nil || !strings.Contains(string(remarshaled), "preflightPath") || strings.Contains(string(remarshaled), "preparationPath") {
t.Fatalf("remarshaled legacy metadata = %s, %v", remarshaled, err)
}
if err := json.Unmarshal([]byte(`{"schemaVersion":"weatherreporter.metadata.v99"}`), &decoded); err == nil {
t.Fatal("Unmarshal() error = nil, want unsupported schema version")
}
}
func TestPromptArtifactRequiredFieldsAreRejected(t *testing.T) {
store := newTestStore(t)
resolved := resolveDailyAt(t, "2026-05-29T05:00:00-05:00")
if _, err := store.SavePromptPreparation(context.Background(), resolved, PromptPreparationArtifact{}); err == nil {
t.Fatal("SavePromptPreparation() error = nil, want required-field error")
}
if _, err := store.SavePromptExecution(context.Background(), resolved, PromptExecutionArtifact{}); err == nil {
t.Fatal("SavePromptExecution() error = nil, want required-field error")
}
paths, err := store.Paths(resolved)
if err != nil {
t.Fatalf("Paths() error = %v", err)
}
metadata := resolved.Metadata()
if _, err := store.SaveMetadata(context.Background(), Metadata{
SchemaVersion: MetadataSchemaVersion,
RunID: metadata.RunID,
MetadataPath: paths.Metadata,
ReportID: metadata.ReportID,
PromptID: metadata.PromptID,
ModuleSnapshotPath: paths.ModuleSnapshot,
DataPackagePath: paths.DataPackage,
}); err == nil {
t.Fatal("SaveMetadata() error = nil, want v2 preparation-path error")
}
}
func TestDailyPathsUseRunIDValidDateDisambiguator(t *testing.T) {
store := newTestStore(t)
first := resolveDailyForDateAt(t, "2026-05-29T05:00:00-05:00", "2026-05-31T12:00:00-05:00")
@@ -960,6 +1103,8 @@ func pathsString(paths ArtifactPaths) string {
paths.Metadata,
paths.ModuleSnapshot,
paths.DataPackage,
paths.Preparation,
paths.Execution,
paths.Preflight,
paths.Notification,
paths.RenderedReport,