package state import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "time" "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/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) func TestPathsUseRunIDAndWorkspace(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) } for _, want := range []string{ 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", "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"), } { if !strings.Contains(pathsString(paths), want) { t.Fatalf("paths = %#v, want component %q", paths, want) } } } 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") second := resolveDailyForDateAt(t, "2026-05-29T05:00:00-05:00", "2026-06-01T12:00:00-05:00") firstPaths, err := store.Paths(first) if err != nil { t.Fatalf("Paths(first) error = %v", err) } secondPaths, err := store.Paths(second) if err != nil { t.Fatalf("Paths(second) error = %v", err) } if first.Metadata().RunID == second.Metadata().RunID { t.Fatalf("RunIDs both = %q, want distinct daily run ids", first.Metadata().RunID) } for name, values := range map[string][2]string{ "RenderedReport": {firstPaths.RenderedReport, secondPaths.RenderedReport}, "Metadata": {firstPaths.Metadata, secondPaths.Metadata}, "DataPackage": {firstPaths.DataPackage, secondPaths.DataPackage}, "Notification": {firstPaths.Notification, secondPaths.Notification}, } { if values[0] == values[1] { t.Fatalf("%s paths both = %q, want distinct daily artifact paths", name, values[0]) } } } func TestBatchDistributorNotificationPathUsesWorkspaceBatchDateAndRunID(t *testing.T) { store := newTestStore(t) location := mustLoadStateLocation(t, "America/Chicago") startedAt := time.Date(2026, 6, 18, 3, 30, 0, 123456789, time.UTC) path, err := store.BatchDistributorNotificationPath(BatchDistributorNotificationRef{ Batch: "evening", BatchRunID: "20260618T033000.123456789Z_evening", StartedAt: startedAt, Location: location, }) if err != nil { t.Fatalf("BatchDistributorNotificationPath() error = %v", err) } want := filepath.Join("notifications", "batches", "evening", "2026-06-17", "distributor.20260618T033000.123456789Z_evening.json") if !strings.Contains(path, want) { t.Fatalf("path = %q, want component %q", path, want) } if !strings.HasPrefix(path, store.root) { t.Fatalf("path = %q, want workspace root prefix %q", path, store.root) } } func TestSaveBatchDistributorNotificationRoundTrip(t *testing.T) { store := newTestStore(t) location := mustLoadStateLocation(t, "America/Chicago") startedAt := time.Date(2026, 6, 17, 12, 0, 0, 0, time.UTC) bundleCreated := startedAt.Add(2 * time.Second) attemptedAt := startedAt.Add(3 * time.Second) acceptedAt := startedAt.Add(4 * time.Second) finishedAt := startedAt.Add(5 * time.Second) ref := BatchDistributorNotificationRef{ Batch: "morning", BatchRunID: "20260617T120000.000000000Z_morning", StartedAt: startedAt, Location: location, } path, err := store.SaveBatchDistributorNotification(context.Background(), ref, BatchDistributorNotificationArtifact{ AttemptedAt: attemptedAt, Endpoint: "https://distributor.example.test", PipelineID: "weatherreporter", BundleID: "weatherreporter.home.morning", IdempotencyKey: "weatherreporter.home.morning.20260617T120000.000000000Z_morning", BundleCreated: bundleCreated, Reports: []BatchDistributorNotificationReportArtifact{ { ReportID: report.Today, RunID: "20260617T120000.000000000Z_today", SourcePath: "/workspace/reports/today/2026-06-17/report.20260617T120000.000000000Z_today.md", BundlePaths: []string{"2026-06-17/today/report.md"}, }, { ReportID: report.Daily, RunID: "20260617T120000.000000000Z_daily_2026-06-19", SourcePath: "/workspace/reports/daily/2026-06-19/report.20260617T120000.000000000Z_daily_2026-06-19.md", BundlePaths: []string{"2026-06-19/daily/report.md"}, }, }, Status: "failed", Upload: &DistributorUploadResult{ RunID: "distributor-run", Status: "accepted", }, RunStatus: &DistributorRunStatus{ RunID: "distributor-run", PipelineID: "weatherreporter", Status: "failed", AcceptedAt: acceptedAt, FinishedAt: &finishedAt, Report: json.RawMessage(`{"actions":[{"action":"failed"}]}`), Error: "destination conflict", }, StatusError: "status lookup failed", Error: "batch upload failed", }) if err != nil { t.Fatalf("SaveBatchDistributorNotification() error = %v", err) } wantPath := filepath.Join("notifications", "batches", "morning", "2026-06-17", "distributor.20260617T120000.000000000Z_morning.json") if !strings.Contains(path, wantPath) { t.Fatalf("path = %q, want component %q", path, wantPath) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("read batch notification: %v", err) } var artifact BatchDistributorNotificationArtifact if err := json.Unmarshal(data, &artifact); err != nil { t.Fatalf("decode batch notification: %v", err) } if artifact.SchemaVersion != BatchDistributorNotificationSchemaVersion { t.Fatalf("SchemaVersion = %q, want %q", artifact.SchemaVersion, BatchDistributorNotificationSchemaVersion) } if artifact.Batch != "morning" || artifact.BatchRunID != ref.BatchRunID { t.Fatalf("artifact batch identity = %q/%q, want ref values", artifact.Batch, artifact.BatchRunID) } if artifact.Endpoint != "https://distributor.example.test" || artifact.PipelineID != "weatherreporter" || artifact.BundleID != "weatherreporter.home.morning" || artifact.IdempotencyKey == "" { t.Fatalf("artifact identity = %#v, want distributor identity", artifact) } if len(artifact.Reports) != 2 || artifact.Reports[0].ReportID != report.Today || strings.Join(artifact.Reports[1].BundlePaths, ",") != "2026-06-19/daily/report.md" { t.Fatalf("Reports = %#v, want included report records", artifact.Reports) } if artifact.Upload == nil || artifact.Upload.RunID != "distributor-run" { t.Fatalf("Upload = %#v, want accepted upload result", artifact.Upload) } if artifact.RunStatus == nil || artifact.RunStatus.Status != "failed" || !strings.Contains(string(artifact.RunStatus.Report), "failed") || artifact.RunStatus.FinishedAt == nil { t.Fatalf("RunStatus = %#v, want failed run status with raw report", artifact.RunStatus) } if artifact.StatusError != "status lookup failed" || artifact.Error != "batch upload failed" { t.Fatalf("errors = %q/%q, want persisted error fields", artifact.StatusError, artifact.Error) } } func TestBatchDistributorNotificationPathRejectsInvalidIdentity(t *testing.T) { store := newTestStore(t) location := mustLoadStateLocation(t, "America/Chicago") valid := BatchDistributorNotificationRef{ Batch: "morning", BatchRunID: "20260617T120000.000000000Z_morning", StartedAt: time.Date(2026, 6, 17, 12, 0, 0, 0, time.UTC), Location: location, } tests := []struct { name string mutate func(*BatchDistributorNotificationRef) wantErr string }{ { name: "Batch", mutate: func(ref *BatchDistributorNotificationRef) { ref.Batch = "" }, wantErr: "batch kind is required", }, { name: "BatchSeparator", mutate: func(ref *BatchDistributorNotificationRef) { ref.Batch = "../morning" }, wantErr: "batch kind must not contain path separators", }, { name: "BatchRunID", mutate: func(ref *BatchDistributorNotificationRef) { ref.BatchRunID = "" }, wantErr: "batch run id is required", }, { name: "BatchRunIDSeparator", mutate: func(ref *BatchDistributorNotificationRef) { ref.BatchRunID = "nested/run" }, wantErr: "batch run id must not contain path separators", }, { name: "StartedAt", mutate: func(ref *BatchDistributorNotificationRef) { ref.StartedAt = time.Time{} }, wantErr: "batch started time is required", }, { name: "Location", mutate: func(ref *BatchDistributorNotificationRef) { ref.Location = nil }, wantErr: "batch location is required", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ref := valid tt.mutate(&ref) _, err := store.BatchDistributorNotificationPath(ref) if err == nil { t.Fatal("BatchDistributorNotificationPath() error = nil, want error") } if !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("error = %q, want %q", err.Error(), tt.wantErr) } }) } } func TestGeneratedTextArtifactPathsUseSnapshotTree(t *testing.T) { store := newTestStore(t) tests := []struct { name string resolved report.Resolved group string validDate string runID string }{ { name: "daily", resolved: resolveDailyAt(t, "2026-05-29T05:00:00-05:00"), group: "daily", validDate: "2026-05-29", runID: "20260529T100000.000000000Z_daily_2026-05-29", }, { name: "hourly", resolved: resolveHourlyAt(t, "2026-05-29T05:00:00-05:00"), group: "hourly", validDate: "2026-05-29", runID: "20260529T100000.000000000Z_hourly", }, { name: "tomorrow", resolved: resolveTomorrowAt(t, "2026-05-29T18:00:00-05:00"), group: "tomorrow", validDate: "2026-05-30", runID: "20260529T230000.000000000Z_tomorrow", }, { name: "today", resolved: resolveTodayAt(t, "2026-05-29T05:00:00-05:00"), group: "today", validDate: "2026-05-29", runID: "20260529T100000.000000000Z_today", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { paths, err := store.Paths(tt.resolved) if err != nil { t.Fatalf("Paths() error = %v", err) } wants := map[string]string{ "ModuleSnapshot": filepath.Join("snapshots", tt.group, tt.validDate, "modules."+tt.runID+".json"), "Metadata": filepath.Join("snapshots", tt.group, tt.validDate, "metadata."+tt.runID+".json"), "DataPackage": filepath.Join("data-packages", tt.group, tt.validDate, "data_package."+tt.runID+".yaml"), "Preflight": filepath.Join("preflight", tt.group, tt.validDate, "render."+tt.runID+".json"), "Notification": filepath.Join("notifications", tt.group, tt.validDate, "distributor."+tt.runID+".json"), "RenderedReport": filepath.Join("reports", tt.group, tt.validDate, "report."+tt.runID+".md"), "GeneratedTextRaw": filepath.Join("snapshots", tt.group, tt.validDate, "generated_text_raw."+tt.runID+".json"), "GeneratedTextResult": filepath.Join("snapshots", tt.group, tt.validDate, "generated_text_result."+tt.runID+".json"), "GeneratedText": filepath.Join("snapshots", tt.group, tt.validDate, "generated_text."+tt.runID+".json"), "RenderContext": filepath.Join("snapshots", tt.group, tt.validDate, "render_context."+tt.runID+".json"), } got := map[string]string{ "ModuleSnapshot": paths.ModuleSnapshot, "Metadata": paths.Metadata, "DataPackage": paths.DataPackage, "Preflight": paths.Preflight, "Notification": paths.Notification, "RenderedReport": paths.RenderedReport, "GeneratedTextRaw": paths.GeneratedTextRaw, "GeneratedTextResult": paths.GeneratedTextResult, "GeneratedText": paths.GeneratedText, "RenderContext": paths.RenderContext, } for name, want := range wants { if !strings.Contains(got[name], want) { t.Fatalf("%s path = %q, want component %q", name, got[name], want) } } }) } } func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) { store := newTestStore(t) resolved := resolveDailyAt(t, "2026-05-29T05:00:00-05:00") briefingMetadata := stateBriefingMetadata(resolved) snapshot, err := module.NewSnapshot([]module.Output{{ ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": resolved.Metadata().RunID}, PromptValue: map[string]string{"prompt_run_id": resolved.Metadata().RunID}, }}) if err != nil { t.Fatalf("NewSnapshot() error = %v", err) } dataPackage, err := promptinput.Build(promptinput.BuildRequest{ Metadata: promptinput.Metadata{ RunID: resolved.Metadata().RunID, ReportID: resolved.Definition.ID, Variant: briefingMetadata.Variant, PromptID: resolved.Definition.PromptID, GeneratedAt: resolved.GeneratedAt, Timezone: resolved.Timezone, ValidPeriod: resolved.ValidPeriod, }, Modules: snapshot, }) if err != nil { t.Fatalf("Build() error = %v", err) } dataPackagePath, err := store.SaveDataPackage(context.Background(), resolved, dataPackage) if err != nil { t.Fatalf("SaveDataPackage() error = %v", err) } moduleSnapshotPath, err := store.SaveModuleSnapshot(context.Background(), resolved, snapshot) if err != nil { t.Fatalf("SaveModuleSnapshot() error = %v", err) } preflightPath, err := store.SavePreflight(context.Background(), resolved, PreflightArtifact{Stdout: `{"ok":true}`}) if err != nil { t.Fatalf("SavePreflight() error = %v", err) } notificationPath, err := store.SaveDistributorNotification(context.Background(), resolved, DistributorNotificationArtifact{ RunID: resolved.Metadata().RunID, ReportID: resolved.Definition.ID, AttemptedAt: resolved.GeneratedAt, Endpoint: "https://distributor.example.test", PipelineID: "weatherreporter.daily", BundleID: "weatherreporter.home.daily.run", IdempotencyKey: "weatherreporter.home.daily.run", SourcePath: "/tmp/report.md", BundlePaths: []string{"2026-05-29/daily/report.md"}, BundleCreated: resolved.GeneratedAt, Status: "succeeded", RunStatus: &DistributorRunStatus{RunID: "distributor-run", Status: "succeeded"}, }) if err != nil { t.Fatalf("SaveDistributorNotification() error = %v", err) } renderedReportPath, err := store.PrepareRenderedReport(context.Background(), resolved) if err != nil { t.Fatalf("PrepareRenderedReport() error = %v", err) } if err := os.WriteFile(renderedReportPath, []byte("# Daily Report\n"), 0o600); err != nil { t.Fatalf("write rendered report: %v", err) } var preflight PreflightArtifact preflightData, err := os.ReadFile(preflightPath) if err != nil { t.Fatalf("read preflight: %v", err) } if err := json.Unmarshal(preflightData, &preflight); err != nil { t.Fatalf("decode preflight: %v", err) } if preflight.Stdout != `{"ok":true}` { t.Fatalf("preflight stdout = %q, want render stdout", preflight.Stdout) } var notification DistributorNotificationArtifact notificationData, err := os.ReadFile(notificationPath) if err != nil { t.Fatalf("read notification: %v", err) } if err := json.Unmarshal(notificationData, ¬ification); err != nil { t.Fatalf("decode notification: %v", err) } if notification.SchemaVersion != DistributorNotificationSchemaVersion || notification.PipelineID != "weatherreporter.daily" || len(notification.BundlePaths) != 1 || notification.RunStatus == nil || notification.RunStatus.Status != "succeeded" { t.Fatalf("notification = %#v, want persisted distributor status", notification) } paths, err := store.Paths(resolved) if err != nil { t.Fatalf("Paths() error = %v", err) } metadata := BuildMetadataFromBriefingMetadata(resolved, briefingMetadata, ArtifactPaths{ ModuleSnapshot: moduleSnapshotPath, Metadata: paths.Metadata, DataPackage: dataPackagePath, Preflight: preflightPath, RenderedReport: renderedReportPath, }) metadataPath, err := store.SaveMetadata(context.Background(), metadata) if err != nil { t.Fatalf("SaveMetadata() error = %v", err) } for _, path := range []string{moduleSnapshotPath, dataPackagePath, preflightPath, notificationPath, renderedReportPath, metadataPath} { if _, err := os.Stat(path); err != nil { t.Fatalf("expected artifact %q: %v", path, err) } } loadedSnapshot, err := store.LoadModuleSnapshot(context.Background(), moduleSnapshotPath) if err != nil { t.Fatalf("LoadModuleSnapshot() error = %v", err) } if loadedSnapshot.SchemaVersion != module.SnapshotSchemaVersion || len(loadedSnapshot.Outputs) != 1 { t.Fatalf("loaded module snapshot = %#v, want one metadata output", loadedSnapshot) } if loadedSnapshot.Outputs[0].PromptValue != nil { t.Fatalf("loaded module snapshot PromptValue = %#v, want omitted runtime value", loadedSnapshot.Outputs[0].PromptValue) } snapshotData, err := os.ReadFile(moduleSnapshotPath) if err != nil { t.Fatalf("read module snapshot: %v", err) } if strings.Contains(string(snapshotData), "prompt_run_id") || strings.Contains(string(snapshotData), "promptValue") || strings.Contains(string(snapshotData), "PromptValue") { t.Fatalf("module snapshot JSON includes runtime-only prompt value:\n%s", string(snapshotData)) } loadedDataPackage, err := store.LoadDataPackage(context.Background(), dataPackagePath) if err != nil { t.Fatalf("LoadDataPackage() error = %v", err) } if loadedDataPackage.SchemaVersion != promptinput.SchemaVersion || loadedDataPackage.Briefing.Order[0] != "metadata" { t.Fatalf("loaded data package = %#v, want YAML package with metadata stanza", loadedDataPackage) } metadataStanza, ok := loadedDataPackage.Briefing.Values["metadata"].(map[string]any) if !ok || metadataStanza["prompt_run_id"] != resolved.Metadata().RunID { t.Fatalf("loaded data package metadata = %#v, want runtime prompt value", loadedDataPackage.Briefing.Values["metadata"]) } var decoded Metadata data, err := os.ReadFile(metadataPath) if err != nil { t.Fatalf("read metadata: %v", err) } if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("decode metadata: %v", err) } if decoded.RunID != resolved.Metadata().RunID { t.Fatalf("RunID = %q, want %q", decoded.RunID, resolved.Metadata().RunID) } if decoded.ModuleSnapshotPath != moduleSnapshotPath || decoded.DataPackagePath != dataPackagePath || decoded.PreflightPath != preflightPath { t.Fatalf("metadata paths = %#v, want saved artifact paths", decoded) } if decoded.RenderedReportPath != renderedReportPath { t.Fatalf("RenderedReportPath = %q, want %q", decoded.RenderedReportPath, renderedReportPath) } if decoded.Location == nil || decoded.Location.Name != "Brentwood" || decoded.Location.Timezone != "America/Chicago" { t.Fatalf("metadata location = %#v, want briefing location", decoded.Location) } if strings.Contains(string(data), "MetadataPath") || strings.Contains(string(data), "metadataPath") { t.Fatalf("metadata JSON includes runtime-only MetadataPath:\n%s", string(data)) } if decoded.GeneratedTextSchemaID != "daily" { t.Fatalf("GeneratedTextSchemaID = %q, want daily", decoded.GeneratedTextSchemaID) } for _, unexpected := range []string{"generatedTextRawPath", "generatedTextResultPath", "generatedTextPath", "renderContextPath"} { if strings.Contains(string(data), unexpected) { t.Fatalf("metadata JSON includes unsaved generated-text path field %q:\n%s", unexpected, string(data)) } } } func TestSaveGeneratedTextArtifactsAndMetadataRoundTrip(t *testing.T) { store := newTestStore(t) resolved := resolveHourlyAt(t, "2026-05-29T05:00:00-05:00") briefingMetadata := stateBriefingMetadata(resolved) rawPath, err := store.SaveGeneratedTextRaw(context.Background(), resolved, []byte(`{"summary":"raw"}`)) if err != nil { t.Fatalf("SaveGeneratedTextRaw() error = %v", err) } resultPath, err := store.SaveGeneratedTextResult(context.Background(), resolved, map[string]any{ "command": []string{"scriptorium", "run"}, "status": "succeeded", }) if err != nil { t.Fatalf("SaveGeneratedTextResult() error = %v", err) } generatedPath, err := store.SaveGeneratedText(context.Background(), resolved, []byte(`{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled."}`)) if err != nil { t.Fatalf("SaveGeneratedText() error = %v", err) } contextPath, err := store.SaveRenderContext(context.Background(), resolved, struct { ReportTitle string `json:"reportTitle"` Location string `json:"location"` }{ ReportTitle: "Hourly Report", Location: "Brentwood", }) if err != nil { t.Fatalf("SaveRenderContext() error = %v", err) } for _, path := range []string{rawPath, resultPath, generatedPath, contextPath} { if _, err := os.Stat(path); err != nil { t.Fatalf("expected generated-text artifact %q: %v", path, err) } } rawData, err := store.LoadGeneratedText(context.Background(), rawPath) if err != nil { t.Fatalf("LoadGeneratedText() raw error = %v", err) } if string(rawData) != `{"summary":"raw"}` { t.Fatalf("raw data = %q, want saved bytes", rawData) } generatedData, err := store.LoadGeneratedText(context.Background(), generatedPath) if err != nil { t.Fatalf("LoadGeneratedText() generated error = %v", err) } if !strings.Contains(string(generatedData), `"forecast_discussion":"A front will keep the region unsettled."`) { t.Fatalf("generated text = %q, want saved normalized JSON", generatedData) } var runResult struct { Command []string `json:"command"` Status string `json:"status"` } if err := store.LoadGeneratedTextResult(context.Background(), resultPath, &runResult); err != nil { t.Fatalf("LoadGeneratedTextResult() error = %v", err) } if runResult.Status != "succeeded" || strings.Join(runResult.Command, " ") != "scriptorium run" { t.Fatalf("run result = %#v, want saved result", runResult) } var renderContext struct { ReportTitle string `json:"reportTitle"` Location string `json:"location"` } if err := store.LoadRenderContext(context.Background(), contextPath, &renderContext); err != nil { t.Fatalf("LoadRenderContext() error = %v", err) } if renderContext.ReportTitle != "Hourly Report" || renderContext.Location != "Brentwood" { t.Fatalf("render context = %#v, want saved context", renderContext) } paths, err := store.Paths(resolved) if err != nil { t.Fatalf("Paths() error = %v", err) } metadata := BuildMetadataFromBriefingMetadata(resolved, briefingMetadata, ArtifactPaths{ ModuleSnapshot: paths.ModuleSnapshot, Metadata: paths.Metadata, DataPackage: paths.DataPackage, Preflight: paths.Preflight, RenderedReport: paths.RenderedReport, GeneratedTextRaw: rawPath, GeneratedTextResult: resultPath, GeneratedText: generatedPath, RenderContext: contextPath, }) metadataPath, err := store.SaveMetadata(context.Background(), metadata) if err != nil { t.Fatalf("SaveMetadata() error = %v", err) } var decoded Metadata metadataData, err := os.ReadFile(metadataPath) if err != nil { t.Fatalf("read metadata: %v", err) } if err := json.Unmarshal(metadataData, &decoded); err != nil { t.Fatalf("decode metadata: %v", err) } if decoded.GeneratedTextSchemaID != "hourly" { t.Fatalf("GeneratedTextSchemaID = %q, want hourly", decoded.GeneratedTextSchemaID) } if decoded.GeneratedTextRawPath != rawPath || decoded.GeneratedTextResultPath != resultPath || decoded.GeneratedTextPath != generatedPath || decoded.RenderContextPath != contextPath { t.Fatalf("metadata generated-text paths = %#v, want saved artifact paths", decoded) } } func TestSaveMetadataUsesExplicitMetadataPath(t *testing.T) { store := newTestStore(t) resolved := resolveDailyAt(t, "2026-05-29T05:00:00-05:00") briefingMetadata := stateBriefingMetadata(resolved) paths, err := store.Paths(resolved) if err != nil { t.Fatalf("Paths() error = %v", err) } otherDir := filepath.Join(t.TempDir(), "other-artifacts") derivedMetadataPath := filepath.Join(otherDir, "metadata."+resolved.Metadata().RunID+".json") metadata := BuildMetadataFromBriefingMetadata(resolved, briefingMetadata, ArtifactPaths{ ModuleSnapshot: paths.ModuleSnapshot, Metadata: paths.Metadata, DataPackage: paths.DataPackage, Preflight: paths.Preflight, RenderedReport: paths.RenderedReport, }) metadataPath, err := store.SaveMetadata(context.Background(), metadata) if err != nil { t.Fatalf("SaveMetadata() error = %v", err) } if metadataPath != paths.Metadata { t.Fatalf("SaveMetadata() path = %q, want explicit metadata path %q", metadataPath, paths.Metadata) } if _, err := os.Stat(paths.Metadata); err != nil { t.Fatalf("expected explicit metadata path %q: %v", paths.Metadata, err) } if _, err := os.Stat(derivedMetadataPath); !os.IsNotExist(err) { t.Fatalf("derived metadata path stat error = %v, want not exist", err) } } func TestFindPriorSnapshot(t *testing.T) { store := newTestStore(t) first := resolveDailyAt(t, "2026-05-29T05:00:00-05:00") second := resolveDailyAt(t, "2026-05-29T08:00:00-05:00") paths := savePriorMetadata(t, store, first, stateBriefingMetadata(first)) prior, err := store.FindPriorSnapshot(context.Background(), second) if err != nil { t.Fatalf("FindPriorSnapshot() error = %v", err) } if prior == nil { t.Fatal("FindPriorSnapshot() = nil, want prior snapshot") } if prior.Metadata.RunID != first.Metadata().RunID { t.Fatalf("RunID = %q, want %q", prior.Metadata.RunID, first.Metadata().RunID) } if prior.ModuleSnapshotPath != paths.ModuleSnapshot { t.Fatalf("ModuleSnapshotPath = %q, want %q", prior.ModuleSnapshotPath, paths.ModuleSnapshot) } } func TestFindPriorSnapshotSupportsToday(t *testing.T) { store := newTestStore(t) first := resolveTodayAt(t, "2026-05-29T05:00:00-05:00") second := resolveTodayAt(t, "2026-05-29T08:00:00-05:00") savePriorMetadata(t, store, first, stateBriefingMetadata(first)) prior, err := store.FindPriorSnapshot(context.Background(), second) if err != nil { t.Fatalf("FindPriorSnapshot() error = %v", err) } if prior == nil { t.Fatal("FindPriorSnapshot() = nil, want prior Today snapshot") } if prior.Metadata.RunID != first.Metadata().RunID { t.Fatalf("RunID = %q, want %q", prior.Metadata.RunID, first.Metadata().RunID) } } func TestFindPriorSnapshotUsesValidDate(t *testing.T) { store := newTestStore(t) previousDate := resolveDailyAt(t, "2026-05-28T05:00:00-05:00") currentDate := resolveDailyAt(t, "2026-05-29T05:00:00-05:00") savePriorMetadata(t, store, previousDate, stateBriefingMetadata(previousDate)) prior, err := store.FindPriorSnapshot(context.Background(), currentDate) if err != nil { t.Fatalf("FindPriorSnapshot() error = %v", err) } if prior != nil { t.Fatalf("FindPriorSnapshot() = %#v, want nil for different valid date", prior) } } func TestFindPriorSnapshotSupportsThreeDay(t *testing.T) { store := newTestStore(t) first := resolveThreeDayAt(t, "2026-05-29T05:00:00-05:00") second := resolveThreeDayAt(t, "2026-05-29T08:00:00-05:00") savePriorMetadata(t, store, first, stateBriefingMetadata(first)) prior, err := store.FindPriorSnapshot(context.Background(), second) if err != nil { t.Fatalf("FindPriorSnapshot() error = %v", err) } if prior == nil { t.Fatal("FindPriorSnapshot() = nil, want prior 3-day snapshot") } if prior.Metadata.RunID != first.Metadata().RunID { t.Fatalf("RunID = %q, want %q", prior.Metadata.RunID, first.Metadata().RunID) } } func TestFindPriorSnapshotSupportsWeekend(t *testing.T) { store := newTestStore(t) first := resolveWeekendAt(t, "2026-05-29T05:00:00-05:00") second := resolveWeekendAt(t, "2026-05-29T08:00:00-05:00") savePriorMetadata(t, store, first, stateBriefingMetadata(first)) prior, err := store.FindPriorSnapshot(context.Background(), second) if err != nil { t.Fatalf("FindPriorSnapshot() error = %v", err) } if prior == nil { t.Fatal("FindPriorSnapshot() = nil, want prior weekend snapshot") } if prior.Metadata.RunID != first.Metadata().RunID { t.Fatalf("RunID = %q, want %q", prior.Metadata.RunID, first.Metadata().RunID) } } func TestFindPriorSnapshotSupportsNarrowedWeekendPeriod(t *testing.T) { store := newTestStore(t) first := resolveWeekendAt(t, "2026-05-29T19:00:00-05:00") second := resolveWeekendAt(t, "2026-05-30T08:00:00-05:00") savePriorMetadata(t, store, first, stateBriefingMetadata(first)) prior, err := store.FindPriorSnapshot(context.Background(), second) if err != nil { t.Fatalf("FindPriorSnapshot() error = %v", err) } if prior == nil { t.Fatal("FindPriorSnapshot() = nil, want prior narrowed weekend snapshot") } if prior.Metadata.RunID != first.Metadata().RunID { t.Fatalf("RunID = %q, want %q", prior.Metadata.RunID, first.Metadata().RunID) } } func TestFindPriorSnapshotIgnoresRollingWindowReports(t *testing.T) { store := newTestStore(t) first := resolveHourlyAt(t, "2026-05-29T05:00:00-05:00") second := resolveHourlyAt(t, "2026-05-29T06:00:00-05:00") savePriorMetadata(t, store, first, stateBriefingMetadata(first)) prior, err := store.FindPriorSnapshot(context.Background(), second) if err != nil { t.Fatalf("FindPriorSnapshot() error = %v", err) } if prior != nil { t.Fatalf("FindPriorSnapshot() = %#v, want nil for rolling-window comparison", prior) } } func TestFilesystemStoreRejectsUnsafeDirs(t *testing.T) { cfg := config.Defaults().Workspace cfg.Root = t.TempDir() cfg.SnapshotsDir = "../snapshots" _, err := NewFilesystemStore(cfg) if err == nil { t.Fatal("NewFilesystemStore() error = nil, want unsafe path error") } if !strings.Contains(err.Error(), "within workspace root") { t.Fatalf("error = %q, want path safety context", err.Error()) } } func newTestStore(t *testing.T) *FilesystemStore { t.Helper() cfg := config.Defaults().Workspace cfg.Root = t.TempDir() store, err := NewFilesystemStore(cfg) if err != nil { t.Fatalf("NewFilesystemStore() error = %v", err) } return store } func mustLoadStateLocation(t *testing.T, name string) *time.Location { t.Helper() location, err := time.LoadLocation(name) if err != nil { t.Fatalf("LoadLocation(%q) error = %v", name, err) } return location } func resolveDailyAt(t *testing.T, value string) report.Resolved { t.Helper() return resolveDailyForDateAt(t, value, value) } func resolveDailyForDateAt(t *testing.T, nowValue string, dateValue string) report.Resolved { t.Helper() location, err := timeutil.LoadLocation("America/Chicago") if err != nil { t.Fatalf("LoadLocation() error = %v", err) } now, err := time.Parse(time.RFC3339, nowValue) if err != nil { t.Fatalf("parse now time: %v", err) } date, err := time.Parse(time.RFC3339, dateValue) if err != nil { t.Fatalf("parse date time: %v", err) } resolved, err := report.DefaultRegistry().Resolve(report.Daily, report.ResolveRequest{ Now: now, Location: location, Date: date, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } return resolved } func resolveTodayAt(t *testing.T, value string) report.Resolved { t.Helper() location, err := timeutil.LoadLocation("America/Chicago") if err != nil { t.Fatalf("LoadLocation() error = %v", err) } now, err := time.Parse(time.RFC3339, value) if err != nil { t.Fatalf("parse time: %v", err) } resolved, err := report.DefaultRegistry().Resolve(report.Today, report.ResolveRequest{ Now: now, Location: location, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } return resolved } func resolveThreeDayAt(t *testing.T, value string) report.Resolved { t.Helper() location, err := timeutil.LoadLocation("America/Chicago") if err != nil { t.Fatalf("LoadLocation() error = %v", err) } now, err := time.Parse(time.RFC3339, value) if err != nil { t.Fatalf("parse time: %v", err) } resolved, err := report.DefaultRegistry().Resolve(report.ThreeDay, report.ResolveRequest{ Now: now, Location: location, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } return resolved } func resolveWeekendAt(t *testing.T, value string) report.Resolved { t.Helper() location, err := timeutil.LoadLocation("America/Chicago") if err != nil { t.Fatalf("LoadLocation() error = %v", err) } now, err := time.Parse(time.RFC3339, value) if err != nil { t.Fatalf("parse time: %v", err) } resolved, err := report.DefaultRegistry().Resolve(report.Weekend, report.ResolveRequest{ Now: now, Location: location, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } return resolved } func resolveHourlyAt(t *testing.T, value string) report.Resolved { t.Helper() location, err := timeutil.LoadLocation("America/Chicago") if err != nil { t.Fatalf("LoadLocation() error = %v", err) } now, err := time.Parse(time.RFC3339, value) if err != nil { t.Fatalf("parse time: %v", err) } resolved, err := report.DefaultRegistry().Resolve(report.Hourly, report.ResolveRequest{ Now: now, Location: location, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } return resolved } func resolveTomorrowAt(t *testing.T, value string) report.Resolved { t.Helper() location, err := timeutil.LoadLocation("America/Chicago") if err != nil { t.Fatalf("LoadLocation() error = %v", err) } now, err := time.Parse(time.RFC3339, value) if err != nil { t.Fatalf("parse time: %v", err) } resolved, err := report.DefaultRegistry().Resolve(report.Tomorrow, report.ResolveRequest{ Now: now, Location: location, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } return resolved } func stateBriefingMetadata(resolved report.Resolved) briefing.Metadata { return briefing.Metadata{ RunID: resolved.Metadata().RunID, ReportID: resolved.Definition.ID, Variant: "today", PromptID: resolved.Definition.PromptID, GeneratedAt: resolved.GeneratedAt, Units: "us", Timezone: resolved.Timezone, Location: &briefing.LocationContext{ ID: "home", Name: "Brentwood", Region: "St. Louis Metro", Timezone: resolved.Timezone, }, ValidPeriod: resolved.ValidPeriod, } } func savePriorMetadata(t *testing.T, store *FilesystemStore, resolved report.Resolved, metadata briefing.Metadata) ArtifactPaths { t.Helper() paths, err := store.Paths(resolved) if err != nil { t.Fatalf("Paths() error = %v", err) } _, err = store.SaveMetadata(context.Background(), BuildMetadataFromBriefingMetadata(resolved, metadata, ArtifactPaths{ ModuleSnapshot: paths.ModuleSnapshot, Metadata: paths.Metadata, DataPackage: paths.DataPackage, Preflight: paths.Preflight, RenderedReport: paths.RenderedReport, })) if err != nil { t.Fatalf("SaveMetadata() error = %v", err) } return paths } func pathsString(paths ArtifactPaths) string { return strings.Join([]string{ paths.Metadata, paths.ModuleSnapshot, paths.DataPackage, paths.Preflight, paths.Notification, paths.RenderedReport, paths.GeneratedTextRaw, paths.GeneratedTextResult, paths.GeneratedText, paths.RenderContext, }, "\n") }