Upgraded the restore command to download previous session artifcats when configured as inputs for the current session analyze stage
This commit is contained in:
180
internal/previouscache/previouscache_test.go
Normal file
180
internal/previouscache/previouscache_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package previouscache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
func TestBuildPlanResolvesPromotedArtifactFromPreviousManifest(t *testing.T) {
|
||||
cfg, paths := previousCacheTestConfig(t)
|
||||
store := &storage.FakeBackend{}
|
||||
seedPreviousCurrent(t, store, cfg, previousManifestWithOutput(t, cfg, "artifacts/session_recap.md", []string{"artifacts/session_recap.md"}))
|
||||
previousPrefix := artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.PreviousSessionID)
|
||||
store.SeedObject(storage.FakeObject{Key: previousPrefix + "artifacts/session_recap.md", Data: []byte("# recap\n")})
|
||||
|
||||
plan, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
|
||||
{Name: "session_recap", Required: true},
|
||||
}, store)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildPlan() error = %v", err)
|
||||
}
|
||||
if plan.PreviousRunID != "previous-run" {
|
||||
t.Fatalf("PreviousRunID = %q, want previous-run", plan.PreviousRunID)
|
||||
}
|
||||
got := recordRelPaths(plan.Records)
|
||||
want := []string{"previous/artifacts/session_recap.md", "previous/manifest.json"}
|
||||
if strings.Join(got, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("record rel paths = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPlanFallsBackToConfiguredOutputPath(t *testing.T) {
|
||||
cfg, paths := previousCacheTestConfig(t)
|
||||
store := &storage.FakeBackend{}
|
||||
seedPreviousCurrent(t, store, cfg, previousManifestWithOutput(t, cfg, "", nil))
|
||||
previousPrefix := artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.PreviousSessionID)
|
||||
store.SeedObject(storage.FakeObject{Key: previousPrefix + "artifacts/session_recap.md", Data: []byte("# recap\n")})
|
||||
|
||||
plan, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
|
||||
{Name: "session_recap", Required: true},
|
||||
}, store)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildPlan() error = %v", err)
|
||||
}
|
||||
if len(plan.Records) != 2 {
|
||||
t.Fatalf("records len = %d, want 2", len(plan.Records))
|
||||
}
|
||||
if plan.Records[0].LocalRelativePath != "previous/artifacts/session_recap.md" {
|
||||
t.Fatalf("artifact local relative path = %q", plan.Records[0].LocalRelativePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPlanSkipsMissingOptionalPreviousArtifact(t *testing.T) {
|
||||
cfg, paths := previousCacheTestConfig(t)
|
||||
store := &storage.FakeBackend{}
|
||||
seedPreviousCurrent(t, store, cfg, previousManifestWithOutput(t, cfg, "", nil))
|
||||
|
||||
plan, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
|
||||
{Name: "session_recap", Required: false},
|
||||
}, store)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildPlan() error = %v", err)
|
||||
}
|
||||
if strings.Join(plan.SkippedMissing, ",") != "session_recap" {
|
||||
t.Fatalf("SkippedMissing = %#v, want session_recap", plan.SkippedMissing)
|
||||
}
|
||||
if len(plan.Records) != 1 || plan.Records[0].Kind != InputKindManifest {
|
||||
t.Fatalf("records = %#v, want manifest only", plan.Records)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPlanMissingRequiredPreviousArtifactFails(t *testing.T) {
|
||||
cfg, paths := previousCacheTestConfig(t)
|
||||
store := &storage.FakeBackend{}
|
||||
seedPreviousCurrent(t, store, cfg, previousManifestWithOutput(t, cfg, "", nil))
|
||||
|
||||
_, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
|
||||
{Name: "session_recap", Required: true},
|
||||
}, store)
|
||||
if err == nil || !strings.Contains(err.Error(), `required previous-session artifact "session_recap" object missing`) {
|
||||
t.Fatalf("BuildPlan() error = %v, want required missing error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPlanValidatesPreviousManifestIdentity(t *testing.T) {
|
||||
cfg, paths := previousCacheTestConfig(t)
|
||||
store := &storage.FakeBackend{}
|
||||
manifest := previousManifestWithOutput(t, cfg, "artifacts/session_recap.md", nil)
|
||||
manifest.SessionID = "wrong-session"
|
||||
seedPreviousCurrent(t, store, cfg, manifest)
|
||||
|
||||
_, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
|
||||
{Name: "session_recap", Required: true},
|
||||
}, store)
|
||||
if err == nil || !strings.Contains(err.Error(), "does not match configured previous_session_id") {
|
||||
t.Fatalf("BuildPlan() error = %v, want identity validation error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func previousCacheTestConfig(t *testing.T) (*config.Config, artifacts.SessionPaths) {
|
||||
t.Helper()
|
||||
workspaceRoot := t.TempDir()
|
||||
cfg := &config.Config{
|
||||
Pipeline: &config.PipelineConfig{
|
||||
Workspace: config.WorkspaceConfig{Root: workspaceRoot},
|
||||
Storage: config.StorageConfig{S3: &config.StorageS3Config{
|
||||
Bucket: "test-bucket",
|
||||
RootPrefix: "dnd",
|
||||
}},
|
||||
Scriptorium: &config.ScriptoriumConfig{Artifacts: map[string]config.ScriptoriumArtifactConfig{
|
||||
"session_recap": {
|
||||
Enabled: true,
|
||||
OutputPath: "artifacts/session_recap.md",
|
||||
},
|
||||
}},
|
||||
},
|
||||
Session: &config.SessionConfig{
|
||||
Campaign: "sample-campaign",
|
||||
SessionID: "2026-05-03",
|
||||
PreviousSessionID: "2026-04-26",
|
||||
},
|
||||
}
|
||||
return cfg, artifacts.NewLocalStore(workspaceRoot).SessionPathsFor(cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
}
|
||||
|
||||
func seedPreviousCurrent(t *testing.T, store *storage.FakeBackend, cfg *config.Config, m *manifest.Manifest) {
|
||||
t.Helper()
|
||||
previousPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID)
|
||||
manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(previousPrefix)
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("previous-run\n")})
|
||||
data, err := marshalManifestForPreviousCacheTest(m)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal manifest: %v", err)
|
||||
}
|
||||
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: data})
|
||||
}
|
||||
|
||||
func previousManifestWithOutput(t *testing.T, cfg *config.Config, rel string, promoted []string) *manifest.Manifest {
|
||||
t.Helper()
|
||||
m := manifest.New(cfg.Session.PreviousSessionID, time.Date(2026, 4, 26, 10, 0, 0, 0, time.UTC))
|
||||
m.Campaign = cfg.Session.Campaign
|
||||
m.RunID = "previous-run"
|
||||
m.LocalWorkDir = filepath.Join("/var/lib/narratio/work", cfg.Session.Campaign, cfg.Session.PreviousSessionID, "runs", m.RunID)
|
||||
if strings.TrimSpace(rel) != "" {
|
||||
m.MarkStageSucceeded("analyze", time.Date(2026, 4, 26, 10, 1, 0, 0, time.UTC), []manifest.ArtifactRecord{
|
||||
{SourceID: "narratio.artifact.session_recap", LocalPath: filepath.Join(filepath.Dir(filepath.Dir(m.LocalWorkDir)), filepath.FromSlash(rel))},
|
||||
})
|
||||
}
|
||||
if promoted != nil {
|
||||
if m.Stages["archive"] == nil {
|
||||
m.MarkStageSucceeded("archive", time.Date(2026, 4, 26, 10, 2, 0, 0, time.UTC), nil)
|
||||
}
|
||||
m.Stages["archive"].Metadata = map[string]any{"promoted_paths": promoted}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func marshalManifestForPreviousCacheTest(m *manifest.Manifest) ([]byte, error) {
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(data, '\n'), nil
|
||||
}
|
||||
|
||||
func recordRelPaths(records []Record) []string {
|
||||
out := make([]string, 0, len(records))
|
||||
for _, record := range records {
|
||||
out = append(out, record.LocalRelativePath)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user