Resolve previous artifacts from commit declarations

This commit is contained in:
2026-09-02 13:53:23 +00:00
parent 6c7a45d8d0
commit 62b1f9b0ae
13 changed files with 127 additions and 311 deletions

View File

@@ -16,18 +16,17 @@ import (
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
func TestBuildPlanResolvesPublishedArtifactFromPreviousManifest(t *testing.T) {
func TestResolveUsesExactCommittedDeclaration(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
seedCommittedPreviousState(t, store, cfg, previousManifestWithOutput(t, cfg, "artifacts/session_recap.md", []string{"artifacts/session_recap.md"}), []artifacts.RemoteArtifact{committedPublishedArtifactForTest("narratio.artifact.session_recap", "artifacts/session_recap.md", []byte("# recap\n"), "recap-generation")})
previousPrefix := artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.PreviousSessionID)
store.SeedObject(storage.FakeObject{Key: previousPrefix + "artifacts/session_recap.md", Data: []byte("# recap\n")})
store.ListErr = context.Canceled
plan, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
plan, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
{Name: "session_recap", Required: true},
}, store)
if err != nil {
t.Fatalf("BuildPlan() error = %v", err)
t.Fatalf("Resolve() error = %v", err)
}
if plan.PreviousRunID != "previous-run" {
t.Fatalf("PreviousRunID = %q, want previous-run", plan.PreviousRunID)
@@ -37,20 +36,22 @@ func TestBuildPlanResolvesPublishedArtifactFromPreviousManifest(t *testing.T) {
if strings.Join(got, ",") != strings.Join(want, ",") {
t.Fatalf("record rel paths = %#v, want %#v", got, want)
}
pointerKey := artifacts.S3CurrentCommitPointerKey(artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.PreviousSessionID))
if strings.Join(store.ExistsKeys, ",") != pointerKey {
t.Fatalf("Exists keys = %#v, want only current pointer %q", store.ExistsKeys, pointerKey)
}
}
func TestBuildPlanFallsBackToConfiguredOutputPath(t *testing.T) {
func TestResolveDerivesLocalPathFromCommittedDestination(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
cfg.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{Enabled: true, OutputPath: "configured/ignored.md"}
store := &storage.FakeBackend{}
seedCommittedPreviousState(t, store, cfg, previousManifestWithOutput(t, cfg, "", nil), []artifacts.RemoteArtifact{committedPublishedArtifactForTest("narratio.artifact.session_recap", "artifacts/session_recap.md", []byte("# recap\n"), "recap-generation")})
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{
plan, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
{Name: "session_recap", Required: true},
}, store)
if err != nil {
t.Fatalf("BuildPlan() error = %v", err)
t.Fatalf("Resolve() error = %v", err)
}
if len(plan.Records) != 2 {
t.Fatalf("records len = %d, want 2", len(plan.Records))
@@ -60,6 +61,18 @@ func TestBuildPlanFallsBackToConfiguredOutputPath(t *testing.T) {
}
}
func TestPublishedOutputRelativePathRejectsUnsafeDestination(t *testing.T) {
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-04-26")
_, err := publishedOutputRelativePath(sessionPrefix, "previous-run", artifacts.S3RunPrefix(sessionPrefix, "previous-run")+"artifacts/session_recap.md")
if err == nil || !strings.Contains(err.Error(), "outside selected run published-output prefix") {
t.Fatalf("publishedOutputRelativePath() error = %v, want prefix rejection", err)
}
_, err = publishedOutputRelativePath(sessionPrefix, "previous-run", artifacts.S3RunPrefix(sessionPrefix, "previous-run")+"published/../session_recap.md")
if err == nil || !strings.Contains(err.Error(), "published output destination") {
t.Fatalf("publishedOutputRelativePath() error = %v, want unsafe destination rejection", err)
}
}
func TestResolveUsesCommittedSourceMappingForCustomDestination(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
@@ -86,8 +99,8 @@ func TestResolveUsesCommittedSourceMappingForCustomDestination(t *testing.T) {
if artifact.RemoteKey != customKey {
t.Fatalf("remote key = %q, want exact configured source destination %q", artifact.RemoteKey, customKey)
}
if artifact.LocalRelativePath != "previous/artifacts/session_recap.md" {
t.Fatalf("local relative path = %q, want source-local cache path", artifact.LocalRelativePath)
if artifact.LocalRelativePath != "previous/artifacts/history/recap-v2.txt" {
t.Fatalf("local relative path = %q, want committed-destination cache path", artifact.LocalRelativePath)
}
}
@@ -100,12 +113,12 @@ func TestResolveDoesNotUseCommittedBasenameFallback(t *testing.T) {
})
_, err := Resolve(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`) {
if err == nil || !strings.Contains(err.Error(), `required previous-session artifact "session_recap" is not declared`) {
t.Fatalf("Resolve() error = %v, want source-mapping absence", err)
}
}
func TestResolveRejectsUndeclaredPublishedCandidates(t *testing.T) {
func TestResolveRejectsUndeclaredPublishedCandidatesWithoutProbing(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
seedPreviousCurrent(t, store, cfg, previousManifestWithOutput(t, cfg, "artifacts/session_recap.md", []string{"archive/session_recap.md"}))
@@ -114,7 +127,7 @@ func TestResolveRejectsUndeclaredPublishedCandidates(t *testing.T) {
store.SeedObject(storage.FakeObject{Key: prefix + "archive/session_recap.md", Data: []byte("published candidate")})
_, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{{Name: "session_recap", Required: true}}, store)
if err == nil || !strings.Contains(err.Error(), "object missing from published candidate keys") {
if err == nil || !strings.Contains(err.Error(), "is not declared by the selected remote commit") {
t.Fatalf("Resolve() error = %v, want undeclared-candidate error", err)
}
}
@@ -130,16 +143,16 @@ func TestResolveHonorsCancellation(t *testing.T) {
}
}
func TestBuildPlanSkipsMissingOptionalPreviousArtifact(t *testing.T) {
func TestResolveSkipsMissingOptionalPreviousArtifact(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{
plan, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
{Name: "session_recap", Required: false},
}, store)
if err != nil {
t.Fatalf("BuildPlan() error = %v", err)
t.Fatalf("Resolve() error = %v", err)
}
if strings.Join(plan.SkippedMissing, ",") != "session_recap" {
t.Fatalf("SkippedMissing = %#v, want session_recap", plan.SkippedMissing)
@@ -149,85 +162,85 @@ func TestBuildPlanSkipsMissingOptionalPreviousArtifact(t *testing.T) {
}
}
func TestBuildPlanMissingRequiredPreviousArtifactFails(t *testing.T) {
func TestResolveMissingRequiredPreviousArtifactFails(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{
_, err := Resolve(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)
if err == nil || !strings.Contains(err.Error(), `required previous-session artifact "session_recap" is not declared`) {
t.Fatalf("Resolve() error = %v, want required missing error", err)
}
}
func TestBuildPlanValidatesPreviousManifestIdentity(t *testing.T) {
func TestResolveValidatesPreviousManifestIdentity(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{
_, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
{Name: "session_recap", Required: true},
}, store)
if err == nil || !strings.Contains(err.Error(), "identity does not match") {
t.Fatalf("BuildPlan() error = %v, want identity validation error", err)
t.Fatalf("Resolve() error = %v, want identity validation error", err)
}
}
func TestBuildPlanMissingCurrentRunPointerSkipsOptionalRequirements(t *testing.T) {
func TestResolveMissingCurrentRunPointerSkipsOptionalRequirements(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
plan, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
plan, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
{Name: "session_recap", Required: false},
}, store)
if err != nil {
t.Fatalf("BuildPlan() error = %v", err)
t.Fatalf("Resolve() error = %v", err)
}
if strings.Join(plan.SkippedMissing, ",") != "session_recap" {
t.Fatalf("SkippedMissing = %#v, want session_recap", plan.SkippedMissing)
}
}
func TestBuildPlanMissingCurrentRunPointerFailsRequiredRequirements(t *testing.T) {
func TestResolveMissingCurrentRunPointerFailsRequiredRequirements(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
_, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
_, err := Resolve(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 commit pointer missing`) {
t.Fatalf("BuildPlan() error = %v, want required missing commit pointer error", err)
t.Fatalf("Resolve() error = %v, want required missing commit pointer error", err)
}
}
func TestBuildPlanMissingCurrentCommitPointerSkipsOptionalRequirements(t *testing.T) {
func TestResolveMissingCurrentCommitPointerSkipsOptionalRequirements(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
plan, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
plan, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
{Name: "session_recap", Required: false},
}, store)
if err != nil {
t.Fatalf("BuildPlan() error = %v", err)
t.Fatalf("Resolve() error = %v", err)
}
if strings.Join(plan.SkippedMissing, ",") != "session_recap" {
t.Fatalf("SkippedMissing = %#v, want session_recap", plan.SkippedMissing)
}
}
func TestBuildPlanMissingCurrentCommitPointerFailsRequiredRequirements(t *testing.T) {
func TestResolveMissingCurrentCommitPointerFailsRequiredRequirements(t *testing.T) {
cfg, paths := previousCacheTestConfig(t)
store := &storage.FakeBackend{}
_, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
_, err := Resolve(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 commit pointer missing`) {
t.Fatalf("BuildPlan() error = %v, want required missing commit pointer error", err)
t.Fatalf("Resolve() error = %v, want required missing commit pointer error", err)
}
}