package state import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/config" "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", "20260529T100000.000000000Z_daily_today.briefing.json"), filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.metadata.json"), filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.data_package.json"), filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.render.json"), filepath.Join("reports", "daily", "20260529T100000.000000000Z_daily_today.md"), } { if !strings.Contains(pathsString(paths), want) { t.Fatalf("paths = %#v, want component %q", paths, want) } } } func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) { store := newTestStore(t) resolved := resolveDailyAt(t, "2026-05-29T05:00:00-05:00") briefingPackage := stateBriefingPackage(resolved) dataPackage, err := promptinput.Build(briefingPackage) if err != nil { t.Fatalf("Build() error = %v", err) } briefingPath, err := store.SaveBriefing(context.Background(), resolved, briefingPackage) if err != nil { t.Fatalf("SaveBriefing() error = %v", err) } dataPackagePath, err := store.SaveDataPackage(context.Background(), resolved, dataPackage) if err != nil { t.Fatalf("SaveDataPackage() error = %v", err) } preflightPath, err := store.SavePreflight(context.Background(), resolved, &scriptorium.RenderResult{Stdout: `{"ok":true}`}) if err != nil { t.Fatalf("SavePreflight() 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) } paths, err := store.Paths(resolved) if err != nil { t.Fatalf("Paths() error = %v", err) } metadata := BuildMetadata(resolved, briefingPackage, ArtifactPaths{ Briefing: briefingPath, 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{briefingPath, dataPackagePath, preflightPath, renderedReportPath, metadataPath} { if _, err := os.Stat(path); err != nil { t.Fatalf("expected artifact %q: %v", path, err) } } loadedBriefing, err := store.LoadBriefing(context.Background(), briefingPath) if err != nil { t.Fatalf("LoadBriefing() error = %v", err) } if loadedBriefing.Metadata.RunID != resolved.Metadata().RunID { t.Fatalf("loaded briefing RunID = %q, want %q", loadedBriefing.Metadata.RunID, resolved.Metadata().RunID) } 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.BriefingPath != briefingPath || 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) } } func TestFindPriorDailySnapshot(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") briefingPackage := stateBriefingPackage(first) briefingPath, err := store.SaveBriefing(context.Background(), first, briefingPackage) if err != nil { t.Fatalf("SaveBriefing() error = %v", err) } paths, err := store.Paths(first) if err != nil { t.Fatalf("Paths() error = %v", err) } _, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{ Briefing: briefingPath, Metadata: paths.Metadata, DataPackage: paths.DataPackage, Preflight: paths.Preflight, RenderedReport: paths.RenderedReport, })) if err != nil { t.Fatalf("SaveMetadata() error = %v", err) } prior, err := store.FindPriorDailySnapshot(context.Background(), second) if err != nil { t.Fatalf("FindPriorDailySnapshot() error = %v", err) } if prior == nil { t.Fatal("FindPriorDailySnapshot() = 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.BriefingPath != briefingPath { t.Fatalf("BriefingPath = %q, want %q", prior.BriefingPath, briefingPath) } } func TestFindPriorDailySnapshotUsesValidDate(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") briefingPackage := stateBriefingPackage(previousDate) briefingPath, err := store.SaveBriefing(context.Background(), previousDate, briefingPackage) if err != nil { t.Fatalf("SaveBriefing() error = %v", err) } paths, err := store.Paths(previousDate) if err != nil { t.Fatalf("Paths() error = %v", err) } _, err = store.SaveMetadata(context.Background(), BuildMetadata(previousDate, briefingPackage, ArtifactPaths{ Briefing: briefingPath, Metadata: paths.Metadata, DataPackage: paths.DataPackage, Preflight: paths.Preflight, RenderedReport: paths.RenderedReport, })) if err != nil { t.Fatalf("SaveMetadata() error = %v", err) } prior, err := store.FindPriorDailySnapshot(context.Background(), currentDate) if err != nil { t.Fatalf("FindPriorDailySnapshot() error = %v", err) } if prior != nil { t.Fatalf("FindPriorDailySnapshot() = %#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") briefingPackage := stateThreeDayBriefingPackage(first) briefingPath, err := store.SaveBriefing(context.Background(), first, briefingPackage) if err != nil { t.Fatalf("SaveBriefing() error = %v", err) } paths, err := store.Paths(first) if err != nil { t.Fatalf("Paths() error = %v", err) } _, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{ Briefing: briefingPath, Metadata: paths.Metadata, DataPackage: paths.DataPackage, Preflight: paths.Preflight, RenderedReport: paths.RenderedReport, })) if err != nil { t.Fatalf("SaveMetadata() error = %v", err) } 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") briefingPackage := stateWeekendBriefingPackage(first) briefingPath, err := store.SaveBriefing(context.Background(), first, briefingPackage) if err != nil { t.Fatalf("SaveBriefing() error = %v", err) } paths, err := store.Paths(first) if err != nil { t.Fatalf("Paths() error = %v", err) } _, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{ Briefing: briefingPath, Metadata: paths.Metadata, DataPackage: paths.DataPackage, Preflight: paths.Preflight, RenderedReport: paths.RenderedReport, })) if err != nil { t.Fatalf("SaveMetadata() error = %v", err) } 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") briefingPackage := stateWeekendBriefingPackage(first) briefingPath, err := store.SaveBriefing(context.Background(), first, briefingPackage) if err != nil { t.Fatalf("SaveBriefing() error = %v", err) } paths, err := store.Paths(first) if err != nil { t.Fatalf("Paths() error = %v", err) } _, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{ Briefing: briefingPath, Metadata: paths.Metadata, DataPackage: paths.DataPackage, Preflight: paths.Preflight, RenderedReport: paths.RenderedReport, })) if err != nil { t.Fatalf("SaveMetadata() error = %v", err) } 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 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 resolveDailyAt(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.DailyToday, 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 stateBriefingPackage(resolved report.Resolved) briefing.Package { return briefing.Package{ Metadata: briefing.Metadata{ SchemaVersion: briefing.SchemaVersion, RunID: resolved.Metadata().RunID, ReportID: resolved.Definition.ID, Variant: "today", PromptID: resolved.Definition.PromptID, GeneratedAt: resolved.GeneratedAt, Units: "us", Timezone: resolved.Timezone, ValidPeriod: resolved.ValidPeriod, }, Daily: &briefing.Daily{ForecastSummaryDate: "2026-05-29"}, } } func stateThreeDayBriefingPackage(resolved report.Resolved) briefing.Package { return briefing.Package{ Metadata: briefing.Metadata{ SchemaVersion: briefing.SchemaVersion, RunID: resolved.Metadata().RunID, ReportID: resolved.Definition.ID, PromptID: resolved.Definition.PromptID, GeneratedAt: resolved.GeneratedAt, Units: "us", Timezone: resolved.Timezone, ValidPeriod: resolved.ValidPeriod, }, ThreeDay: &briefing.ThreeDay{ Days: []briefing.OutlookDay{{Date: "2026-05-29"}}, }, } } func stateWeekendBriefingPackage(resolved report.Resolved) briefing.Package { return briefing.Package{ Metadata: briefing.Metadata{ SchemaVersion: briefing.SchemaVersion, RunID: resolved.Metadata().RunID, ReportID: resolved.Definition.ID, PromptID: resolved.Definition.PromptID, GeneratedAt: resolved.GeneratedAt, Units: "us", Timezone: resolved.Timezone, ValidPeriod: resolved.ValidPeriod, }, Weekend: &briefing.Weekend{ Days: []briefing.OutlookDay{{Date: "2026-05-30"}}, }, } } func pathsString(paths ArtifactPaths) string { return strings.Join([]string{ paths.Briefing, paths.Metadata, paths.DataPackage, paths.Preflight, paths.RenderedReport, }, "\n") }