Centralize remote current-state loading and preserve caller policy

This commit is contained in:
2026-05-23 13:56:53 +00:00
parent a6b0c33e9f
commit 3971443831
8 changed files with 511 additions and 157 deletions

View File

@@ -2,8 +2,8 @@ package previouscache
import (
"context"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"sort"
@@ -92,86 +92,28 @@ func BuildPlan(
campaign,
previousSessionID,
)
currentManifestKey, currentRunIDKey := artifacts.ResolveCurrentStateKeys(previousSessionPrefix)
result := &Plan{}
runPointerExists, err := store.Exists(ctx, currentRunIDKey)
current, err := artifacts.LoadCurrentState(ctx, store, previousSessionPrefix, artifacts.CurrentStateValidation{
ExpectedSessionID: previousSessionID,
ExpectedCampaign: campaign,
ValidateRunID: true,
})
if err != nil {
return nil, fmt.Errorf("check previous-session current run pointer %q: %w", currentRunIDKey, err)
}
if !runPointerExists {
if len(requiredNames) > 0 {
return nil, fmt.Errorf("required previous-session artifacts unavailable: remote current run pointer missing: %q", currentRunIDKey)
var runPointerMissing *artifacts.CurrentRunPointerMissingError
var manifestMissing *artifacts.CurrentManifestMissingError
if errors.As(err, &runPointerMissing) || errors.As(err, &manifestMissing) {
if len(requiredNames) > 0 {
return nil, fmt.Errorf("required previous-session artifacts unavailable: remote %w", err)
}
result.SkippedMissing = optionalNames
return result, nil
}
result.SkippedMissing = optionalNames
return result, nil
}
runIDTemp, err := storage.DownloadObjectToTemp(ctx, store, currentRunIDKey, "narratio-previous-run-id-*.txt")
if err != nil {
return nil, fmt.Errorf("download previous-session current run pointer %q: %w", currentRunIDKey, err)
}
defer func() { _ = os.Remove(runIDTemp) }()
runIDBytes, err := os.ReadFile(runIDTemp)
if err != nil {
return nil, fmt.Errorf("read previous-session current run pointer %q: %w", currentRunIDKey, err)
}
previousRunID := strings.TrimSpace(string(runIDBytes))
if previousRunID == "" {
return nil, fmt.Errorf("previous-session current run pointer %q is empty", currentRunIDKey)
}
result.PreviousRunID = previousRunID
manifestExists, err := store.Exists(ctx, currentManifestKey)
if err != nil {
return nil, fmt.Errorf("check previous-session current manifest %q: %w", currentManifestKey, err)
}
if !manifestExists {
if len(requiredNames) > 0 {
return nil, fmt.Errorf("required previous-session artifacts unavailable: remote current manifest missing: %q", currentManifestKey)
}
result.SkippedMissing = optionalNames
return result, nil
}
manifestTemp, err := storage.DownloadObjectToTemp(ctx, store, currentManifestKey, "narratio-previous-manifest-*.json")
if err != nil {
return nil, fmt.Errorf("download previous-session current manifest %q: %w", currentManifestKey, err)
}
defer func() { _ = os.Remove(manifestTemp) }()
manifestStore := &manifest.LocalStore{}
previousManifest, err := manifestStore.Load(ctx, manifestTemp)
if err != nil {
return nil, fmt.Errorf("decode downloaded previous-session manifest %q: %w", currentManifestKey, err)
}
if strings.TrimSpace(previousManifest.SessionID) != previousSessionID {
return nil, fmt.Errorf(
"previous-session manifest session_id %q does not match configured previous_session_id %q",
strings.TrimSpace(previousManifest.SessionID),
previousSessionID,
)
}
if strings.TrimSpace(previousManifest.Campaign) != campaign {
return nil, fmt.Errorf(
"previous-session manifest campaign %q does not match current campaign %q",
strings.TrimSpace(previousManifest.Campaign),
campaign,
)
}
if strings.TrimSpace(previousManifest.RunID) == "" {
return nil, fmt.Errorf("previous-session manifest run_id is required")
}
if strings.TrimSpace(previousManifest.RunID) != previousRunID {
return nil, fmt.Errorf(
"previous-session current run pointer %q references run %q but current manifest run_id is %q",
currentRunIDKey,
previousRunID,
strings.TrimSpace(previousManifest.RunID),
)
return nil, fmt.Errorf("load previous-session current state: %w", err)
}
result.PreviousRunID = strings.TrimSpace(current.RunID)
currentManifestKey := current.CurrentManifestKey
previousManifest := current.Manifest
manifestRel, err := relativeToSession(paths, paths.PreviousManifestPath)
if err != nil {

View File

@@ -100,11 +100,71 @@ func TestBuildPlanValidatesPreviousManifestIdentity(t *testing.T) {
_, 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") {
if err == nil || !strings.Contains(err.Error(), "does not match expected session_id") {
t.Fatalf("BuildPlan() error = %v, want identity validation error", err)
}
}
func TestBuildPlanMissingCurrentRunPointerSkipsOptionalRequirements(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
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)
}
}
func TestBuildPlanMissingCurrentRunPointerFailsRequiredRequirements(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
_, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
{Name: "session_recap", Required: true},
}, store)
if err == nil || !strings.Contains(err.Error(), `required previous-session artifacts unavailable: remote current run pointer missing`) {
t.Fatalf("BuildPlan() error = %v, want required missing run pointer error", err)
}
}
func TestBuildPlanMissingCurrentManifestSkipsOptionalRequirements(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
previousPrefix := artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.PreviousSessionID)
_, runIDKey := artifacts.ResolveCurrentStateKeys(previousPrefix)
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("previous-run\n")})
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)
}
}
func TestBuildPlanMissingCurrentManifestFailsRequiredRequirements(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
previousPrefix := artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.PreviousSessionID)
_, runIDKey := artifacts.ResolveCurrentStateKeys(previousPrefix)
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("previous-run\n")})
_, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
{Name: "session_recap", Required: true},
}, store)
if err == nil || !strings.Contains(err.Error(), `required previous-session artifacts unavailable: remote current manifest missing`) {
t.Fatalf("BuildPlan() error = %v, want required missing manifest error", err)
}
}
func previousCacheTestConfig(t *testing.T) (*config.Config, artifacts.SessionPaths) {
t.Helper()
workspaceRoot := t.TempDir()