Add durable prompt execution state records
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"
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user