Resolve previous artifacts from commit declarations
This commit is contained in:
@@ -136,8 +136,10 @@ Extraction sources (`narratio.extraction.*`):
|
|||||||
Previous-session sources (`narratio.previous_session.artifact.*`):
|
Previous-session sources (`narratio.previous_session.artifact.*`):
|
||||||
|
|
||||||
- resolve only from local `previous/` cache state;
|
- resolve only from local `previous/` cache state;
|
||||||
- prefer manifest-backed previous-input paths;
|
- are hydrated only from the unique `published_output` declaration for their
|
||||||
- fallback to existing previous-cache filesystem paths.
|
canonical source in the selected remote commit; and
|
||||||
|
- never infer a path from a previous manifest, current configuration, basename,
|
||||||
|
or existing cache file.
|
||||||
|
|
||||||
Source absence is evaluated by the consuming artifact input. An optional input
|
Source absence is evaluated by the consuming artifact input. An optional input
|
||||||
is omitted from that invocation; a required input fails resolution. This is
|
is omitted from that invocation; a required input fails resolution. This is
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Planner behavior:
|
|||||||
|
|
||||||
- a new-protocol restore uses only the selected commit's declared artifact set;
|
- a new-protocol restore uses only the selected commit's declared artifact set;
|
||||||
each action carries that artifact's immutable key, checksum, size, and
|
each action carries that artifact's immutable key, checksum, size, and
|
||||||
generation. Coherent legacy state remains on the isolated compatibility path;
|
generation;
|
||||||
- remote-to-local mapping is traversal-safe;
|
- remote-to-local mapping is traversal-safe;
|
||||||
- actions are sorted by local relative path and then remote key;
|
- actions are sorted by local relative path and then remote key;
|
||||||
- planning opens the selected session root and inspects every target by its
|
- planning opens the selected session root and inspects every target by its
|
||||||
@@ -48,8 +48,11 @@ For a non-dry-run restore, planning/classification happens only after acquiring
|
|||||||
the session lock. Runner manifest/reuse checks acquire that same lock first.
|
the session lock. Runner manifest/reuse checks acquire that same lock first.
|
||||||
|
|
||||||
Previous-cache readiness is resolved through `previouscache.Resolve` for restore,
|
Previous-cache readiness is resolved through `previouscache.Resolve` for restore,
|
||||||
prepare, status, and validation. A committed source is selected only by its
|
prepare, status, and validation. Each requirement selects the unique
|
||||||
exact source identity; legacy fallback remains isolated and rejects ambiguity.
|
`published_output` for its canonical source in the pointer-selected commit,
|
||||||
|
then strips only that run's immutable `published/` prefix for its safe local
|
||||||
|
cache path. It never probes candidate objects or derives paths from manifests,
|
||||||
|
configuration, or basenames.
|
||||||
|
|
||||||
## Execution Contract
|
## Execution Contract
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ Materialize canonical current-session inputs before processing stages.
|
|||||||
- records previous inputs in `manifest.inputs`.
|
- records previous inputs in `manifest.inputs`.
|
||||||
|
|
||||||
Required previous-session inputs fail when unavailable; optional missing inputs
|
Required previous-session inputs fail when unavailable; optional missing inputs
|
||||||
are typed skipped results. Committed sources use their exact source-to-destination
|
are typed skipped results. Each requirement resolves only from its unique
|
||||||
mapping, while the isolated legacy reader rejects ambiguous fallback matches.
|
`published_output` declaration in the pointer-selected remote commit. Prepare
|
||||||
|
strips that selected run's immutable `published/` prefix to derive the local
|
||||||
|
cache path, then validates the committed checksum, size, and generation.
|
||||||
|
|
||||||
## Invariants
|
## Invariants
|
||||||
|
|
||||||
|
|||||||
@@ -436,7 +436,8 @@ Default restore scope:
|
|||||||
|
|
||||||
- the committed session manifest and the committed transcript/artifact objects
|
- the committed session manifest and the committed transcript/artifact objects
|
||||||
declared by the selected remote commit
|
declared by the selected remote commit
|
||||||
- `previous/**` when needed by configured previous-session artifact inputs
|
- `previous/**` when needed by configured previous-session artifact inputs;
|
||||||
|
each is selected from its exact committed `published_output` declaration
|
||||||
|
|
||||||
Optional:
|
Optional:
|
||||||
|
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ and reuse.
|
|||||||
|
|
||||||
## Stage 5 — Resolve Previous Artifacts From Commit Declarations
|
## Stage 5 — Resolve Previous Artifacts From Commit Declarations
|
||||||
|
|
||||||
**Status: Pending**
|
**Status: Completed**
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
|
|||||||
@@ -494,7 +494,9 @@ Symptom:
|
|||||||
Likely causes:
|
Likely causes:
|
||||||
|
|
||||||
- missing `session.previous_session_id`;
|
- missing `session.previous_session_id`;
|
||||||
- previous artifact not restored/published for source session.
|
- the selected previous-session commit does not declare the required canonical
|
||||||
|
source as a `published_output`; or
|
||||||
|
- the declared published object is unavailable or fails identity validation.
|
||||||
|
|
||||||
Diagnostics:
|
Diagnostics:
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ import (
|
|||||||
type FakeBackend struct {
|
type FakeBackend struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
Objects map[string]FakeObject
|
Objects map[string]FakeObject
|
||||||
Uploads []FakeUploadCall
|
Uploads []FakeUploadCall
|
||||||
Downloads []FakeDownloadCall
|
Downloads []FakeDownloadCall
|
||||||
Reads []FakeReadCall
|
Reads []FakeReadCall
|
||||||
|
ExistsKeys []string
|
||||||
|
|
||||||
ListErr error
|
ListErr error
|
||||||
DownloadErr error
|
DownloadErr error
|
||||||
@@ -323,8 +324,12 @@ func (f *FakeBackend) Exists(ctx context.Context, key string) (bool, error) {
|
|||||||
if f.ExistsErr != nil {
|
if f.ExistsErr != nil {
|
||||||
return false, f.ExistsErr
|
return false, f.ExistsErr
|
||||||
}
|
}
|
||||||
|
normalizedKey := normalizeObjectKey(key)
|
||||||
|
f.mu.Lock()
|
||||||
|
f.ExistsKeys = append(f.ExistsKeys, normalizedKey)
|
||||||
|
f.mu.Unlock()
|
||||||
f.mu.RLock()
|
f.mu.RLock()
|
||||||
_, ok := f.Objects[normalizeObjectKey(key)]
|
_, ok := f.Objects[normalizedKey]
|
||||||
f.mu.RUnlock()
|
f.mu.RUnlock()
|
||||||
return ok, nil
|
return ok, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1230,7 +1230,7 @@ func TestExecuteStatusDetectsMissingRequiredPreviousArtifact(t *testing.T) {
|
|||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(stdout.String(), `Previous-session artifacts: unavailable: remote required previous-session artifact "session_recap" object missing`) {
|
if !strings.Contains(stdout.String(), `Previous-session artifacts: unavailable: remote required previous-session artifact "session_recap" is not declared`) {
|
||||||
t.Fatalf("stdout = %q, want missing required previous artifact", stdout.String())
|
t.Fatalf("stdout = %q, want missing required previous artifact", stdout.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -632,7 +632,9 @@ func seedRestorePreviousCurrent(t *testing.T, fake *storage.FakeBackend, cfg *co
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
seedRestorePreviousCurrentManifestOnly(t, fake, cfg)
|
seedRestorePreviousCurrentManifestOnly(t, fake, cfg)
|
||||||
previousPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID)
|
previousPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID)
|
||||||
seedRestoreObject(fake, previousPrefix+"artifacts/session_recap.md", []byte(artifactBody))
|
previousRunID := "20260426T010203Z-a1b2c3d4"
|
||||||
|
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(previousPrefix, previousRunID), "published/artifacts/session_recap.md")
|
||||||
|
seedRestoreObject(fake, key, []byte(artifactBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedRestorePreviousCurrentManifestOnly(t *testing.T, fake *storage.FakeBackend, cfg *config.Config) {
|
func seedRestorePreviousCurrentManifestOnly(t *testing.T, fake *storage.FakeBackend, cfg *config.Config) {
|
||||||
|
|||||||
@@ -380,15 +380,17 @@ func seedRestoreObject(store *storage.FakeBackend, key string, data []byte) {
|
|||||||
}
|
}
|
||||||
destination := key
|
destination := key
|
||||||
if strings.HasPrefix(key, sessionPrefix) && !strings.HasPrefix(key, runPrefix) {
|
if strings.HasPrefix(key, sessionPrefix) && !strings.HasPrefix(key, runPrefix) {
|
||||||
destination = runPrefix + strings.TrimPrefix(key, sessionPrefix)
|
relative := strings.TrimPrefix(key, sessionPrefix)
|
||||||
|
destination = runPrefix + relative
|
||||||
store.SeedObject(storage.FakeObject{Key: destination, Data: data})
|
store.SeedObject(storage.FakeObject{Key: destination, Data: data})
|
||||||
}
|
}
|
||||||
typeID := artifacts.RemoteArtifactTypePublishedOutput
|
typeID := artifacts.RemoteArtifactTypePublishedOutput
|
||||||
source := "narratio.test"
|
source := "narratio.test"
|
||||||
if destination == artifacts.S3RunSessionManifestKey(sessionPrefix, pointer.RunID) {
|
if destination == artifacts.S3RunSessionManifestKey(sessionPrefix, pointer.RunID) {
|
||||||
typeID, source = artifacts.RemoteArtifactTypeSessionManifest, "session.manifest"
|
typeID, source = artifacts.RemoteArtifactTypeSessionManifest, "session.manifest"
|
||||||
} else if relative := strings.TrimPrefix(destination, runPrefix); strings.HasPrefix(relative, "artifacts/") {
|
} else if relative := strings.TrimPrefix(destination, runPrefix); strings.HasPrefix(relative, "artifacts/") || strings.HasPrefix(relative, "published/artifacts/") {
|
||||||
name := strings.TrimSuffix(strings.TrimPrefix(relative, "artifacts/"), filepath.Ext(relative))
|
name := strings.TrimPrefix(relative, "published/")
|
||||||
|
name = strings.TrimSuffix(strings.TrimPrefix(name, "artifacts/"), filepath.Ext(name))
|
||||||
source = artifacts.ConfiguredArtifactSourceID(name)
|
source = artifacts.ConfiguredArtifactSourceID(name)
|
||||||
}
|
}
|
||||||
artifact := remoteRestoreArtifact(store, typeID, source, destination)
|
artifact := remoteRestoreArtifact(store, typeID, source, destination)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -118,7 +117,9 @@ func Resolve(
|
|||||||
}
|
}
|
||||||
result.PreviousRunID = strings.TrimSpace(current.RunID)
|
result.PreviousRunID = strings.TrimSpace(current.RunID)
|
||||||
currentManifestKey := current.CurrentManifestKey
|
currentManifestKey := current.CurrentManifestKey
|
||||||
previousManifest := current.Manifest
|
if current.Commit == nil {
|
||||||
|
return nil, fmt.Errorf("previous-session current state does not include a committed remote declaration")
|
||||||
|
}
|
||||||
|
|
||||||
manifestRel, err := relativeToSession(paths, paths.PreviousManifestPath)
|
manifestRel, err := relativeToSession(paths, paths.PreviousManifestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -131,64 +132,33 @@ func Resolve(
|
|||||||
RemoteKey: currentManifestKey,
|
RemoteKey: currentManifestKey,
|
||||||
S3Bucket: bucket,
|
S3Bucket: bucket,
|
||||||
}
|
}
|
||||||
if current.Commit != nil {
|
committedManifest, ok := current.Commit.Artifact(artifacts.RemoteArtifactTypeSessionManifest)
|
||||||
committedManifest, ok := current.Commit.Artifact(artifacts.RemoteArtifactTypeSessionManifest)
|
if !ok || committedManifest.DestinationKey != currentManifestKey {
|
||||||
if !ok || committedManifest.DestinationKey != currentManifestKey {
|
return nil, fmt.Errorf("previous-session remote commit does not declare its session manifest")
|
||||||
return nil, fmt.Errorf("previous-session remote commit does not declare its session manifest")
|
|
||||||
}
|
|
||||||
manifestRecord.SHA256 = committedManifest.SHA256
|
|
||||||
manifestRecord.Size = committedManifest.Size
|
|
||||||
manifestRecord.Generation = committedManifest.Generation
|
|
||||||
manifestRecord.VerifiedContent = append([]byte(nil), current.ManifestData...)
|
|
||||||
}
|
}
|
||||||
|
manifestRecord.SHA256 = committedManifest.SHA256
|
||||||
|
manifestRecord.Size = committedManifest.Size
|
||||||
|
manifestRecord.Generation = committedManifest.Generation
|
||||||
|
manifestRecord.VerifiedContent = append([]byte(nil), current.ManifestData...)
|
||||||
result.Records = append(result.Records, manifestRecord)
|
result.Records = append(result.Records, manifestRecord)
|
||||||
|
|
||||||
for _, requirement := range orderedRequirements {
|
for _, requirement := range orderedRequirements {
|
||||||
selectedRel := ""
|
sourceID := previousArtifactSourceID(requirement.Name)
|
||||||
selectedKey := ""
|
selectedArtifact, ok := committedPublishedArtifactBySource(current.Commit, sourceID)
|
||||||
var selectedArtifact *artifacts.RemoteArtifact
|
if !ok {
|
||||||
if current.Commit != nil {
|
|
||||||
sourceID := previousArtifactSourceID(requirement.Name)
|
|
||||||
artifact, ok := committedPublishedArtifactBySource(current.Commit, sourceID)
|
|
||||||
if ok {
|
|
||||||
selectedRel, ok = sourceRelativePath(sourceID, requirement.Name, previousManifest, cfg)
|
|
||||||
if ok {
|
|
||||||
selectedKey = artifact.DestinationKey
|
|
||||||
selectedArtifact = &artifact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Legacy current state predates immutable source-to-destination
|
|
||||||
// mappings. Keep its compatibility candidates isolated here.
|
|
||||||
matchedCandidates := make([]string, 0, 1)
|
|
||||||
for _, candidate := range legacyArtifactRelativePathCandidates(requirement.Name, previousManifest, cfg) {
|
|
||||||
remoteKey := artifacts.S3PublishedOutputKey(previousSessionPrefix, candidate)
|
|
||||||
exists, err := store.Exists(ctx, remoteKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("check previous-session artifact object %q: %w", remoteKey, err)
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
matchedCandidates = append(matchedCandidates, candidate)
|
|
||||||
if len(matchedCandidates) == 1 {
|
|
||||||
selectedRel = candidate
|
|
||||||
selectedKey = remoteKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(matchedCandidates) > 1 {
|
|
||||||
return nil, fmt.Errorf("legacy previous-session artifact %q matches multiple published candidates: %s", requirement.Name, strings.Join(matchedCandidates, ", "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if selectedRel == "" {
|
|
||||||
if requirement.Required {
|
if requirement.Required {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"required previous-session artifact %q object missing from published candidate keys",
|
"required previous-session artifact %q is not declared by the selected remote commit",
|
||||||
requirement.Name,
|
requirement.Name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
result.SkippedMissing = append(result.SkippedMissing, requirement.Name)
|
result.SkippedMissing = append(result.SkippedMissing, requirement.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
selectedRel, err := publishedOutputRelativePath(previousSessionPrefix, current.RunID, selectedArtifact.DestinationKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("resolve declared previous-session artifact %q: %w", requirement.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
localPath, err := artifacts.SessionPreviousArtifactPath(paths, selectedRel)
|
localPath, err := artifacts.SessionPreviousArtifactPath(paths, selectedRel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -204,13 +174,11 @@ func Resolve(
|
|||||||
Required: requirement.Required,
|
Required: requirement.Required,
|
||||||
LocalRelativePath: localRel,
|
LocalRelativePath: localRel,
|
||||||
LocalPath: localPath,
|
LocalPath: localPath,
|
||||||
RemoteKey: selectedKey,
|
RemoteKey: selectedArtifact.DestinationKey,
|
||||||
S3Bucket: bucket,
|
S3Bucket: bucket,
|
||||||
}
|
SHA256: selectedArtifact.SHA256,
|
||||||
if selectedArtifact != nil {
|
Size: selectedArtifact.Size,
|
||||||
record.SHA256 = selectedArtifact.SHA256
|
Generation: selectedArtifact.Generation,
|
||||||
record.Size = selectedArtifact.Size
|
|
||||||
record.Generation = selectedArtifact.Generation
|
|
||||||
}
|
}
|
||||||
result.Records = append(result.Records, record)
|
result.Records = append(result.Records, record)
|
||||||
}
|
}
|
||||||
@@ -225,17 +193,6 @@ func Resolve(
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildPlan is retained for callers that need the historical name.
|
|
||||||
func BuildPlan(
|
|
||||||
ctx context.Context,
|
|
||||||
cfg *config.Config,
|
|
||||||
paths artifacts.SessionPaths,
|
|
||||||
requirements []artifacts.PreviousArtifactRequirement,
|
|
||||||
store storage.ObjectStore,
|
|
||||||
) (*Plan, error) {
|
|
||||||
return Resolve(ctx, cfg, paths, requirements, store)
|
|
||||||
}
|
|
||||||
|
|
||||||
func committedPublishedArtifactBySource(commit *artifacts.RemoteCommitManifest, source string) (artifacts.RemoteArtifact, bool) {
|
func committedPublishedArtifactBySource(commit *artifacts.RemoteCommitManifest, source string) (artifacts.RemoteArtifact, bool) {
|
||||||
if commit == nil {
|
if commit == nil {
|
||||||
return artifacts.RemoteArtifact{}, false
|
return artifacts.RemoteArtifact{}, false
|
||||||
@@ -252,6 +209,19 @@ func committedPublishedArtifactBySource(commit *artifacts.RemoteCommitManifest,
|
|||||||
return artifacts.RemoteArtifact{}, false
|
return artifacts.RemoteArtifact{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func publishedOutputRelativePath(sessionPrefix, runID, destinationKey string) (string, error) {
|
||||||
|
prefix := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, runID), "published") + "/"
|
||||||
|
rel, ok := strings.CutPrefix(strings.TrimSpace(destinationKey), prefix)
|
||||||
|
if !ok || strings.TrimSpace(rel) == "" {
|
||||||
|
return "", fmt.Errorf("destination %q is outside selected run published-output prefix %q", destinationKey, prefix)
|
||||||
|
}
|
||||||
|
normalized, err := pathsafe.NormalizeRelativeDestination(rel)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("published output destination %q: %w", destinationKey, err)
|
||||||
|
}
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
func requiredPreviousArtifactNames(requirements []artifacts.PreviousArtifactRequirement) []string {
|
func requiredPreviousArtifactNames(requirements []artifacts.PreviousArtifactRequirement) []string {
|
||||||
names := make([]string, 0, len(requirements))
|
names := make([]string, 0, len(requirements))
|
||||||
for _, requirement := range requirements {
|
for _, requirement := range requirements {
|
||||||
@@ -283,172 +253,6 @@ func previousArtifactSourceID(artifactName string) string {
|
|||||||
return sourceID
|
return sourceID
|
||||||
}
|
}
|
||||||
|
|
||||||
func sourceRelativePath(sourceID, artifactName string, previousManifest *manifest.Manifest, cfg *config.Config) (string, bool) {
|
|
||||||
if rel, ok := manifestArtifactRelativePathBySourceID(previousManifest, sourceID); ok {
|
|
||||||
return rel, true
|
|
||||||
}
|
|
||||||
if cfg != nil && cfg.Pipeline != nil && cfg.Pipeline.Scriptorium != nil {
|
|
||||||
if artifactCfg, ok := cfg.Pipeline.Scriptorium.Artifacts[artifactName]; ok {
|
|
||||||
normalized, err := pathsafe.NormalizeRelativeDestination(artifactCfg.OutputPath)
|
|
||||||
if err == nil {
|
|
||||||
return normalized, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func legacyArtifactRelativePathCandidates(
|
|
||||||
artifactName string,
|
|
||||||
previousManifest *manifest.Manifest,
|
|
||||||
cfg *config.Config,
|
|
||||||
) []string {
|
|
||||||
candidates := []string{}
|
|
||||||
appendCandidate := func(v string) {
|
|
||||||
normalized, err := pathsafe.NormalizeRelativeDestination(v)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
candidates = append(candidates, normalized)
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceID := previousArtifactSourceID(artifactName)
|
|
||||||
if rel, ok := manifestArtifactRelativePathBySourceID(previousManifest, sourceID); ok {
|
|
||||||
appendCandidate(rel)
|
|
||||||
base := filepath.Base(rel)
|
|
||||||
for _, published := range manifestPublishedPaths(previousManifest) {
|
|
||||||
if filepath.Base(published) == base {
|
|
||||||
appendCandidate(published)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg != nil && cfg.Pipeline != nil && cfg.Pipeline.Scriptorium != nil {
|
|
||||||
if artifactCfg, ok := cfg.Pipeline.Scriptorium.Artifacts[artifactName]; ok {
|
|
||||||
appendCandidate(artifactCfg.OutputPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dedupeOrderedStrings(candidates)
|
|
||||||
}
|
|
||||||
|
|
||||||
func manifestArtifactRelativePathBySourceID(previousManifest *manifest.Manifest, sourceID string) (string, bool) {
|
|
||||||
if previousManifest == nil || len(previousManifest.Stages) == 0 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
sourceID = strings.TrimSpace(sourceID)
|
|
||||||
if sourceID == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
stageNames := make([]string, 0, len(previousManifest.Stages))
|
|
||||||
if _, ok := previousManifest.Stages["analyze"]; ok {
|
|
||||||
stageNames = append(stageNames, "analyze")
|
|
||||||
}
|
|
||||||
for stageName := range previousManifest.Stages {
|
|
||||||
if stageName == "analyze" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stageNames = append(stageNames, stageName)
|
|
||||||
}
|
|
||||||
start := 0
|
|
||||||
if len(stageNames) > 0 && stageNames[0] == "analyze" {
|
|
||||||
start = 1
|
|
||||||
}
|
|
||||||
sort.Strings(stageNames[start:])
|
|
||||||
|
|
||||||
for _, stageName := range stageNames {
|
|
||||||
sr := previousManifest.Stages[stageName]
|
|
||||||
if sr == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, out := range sr.Outputs {
|
|
||||||
if strings.TrimSpace(out.SourceID) != sourceID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rel, ok := deriveManifestRelativePath(previousManifest, out.LocalPath)
|
|
||||||
if ok {
|
|
||||||
return rel, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func deriveManifestRelativePath(previousManifest *manifest.Manifest, localPath string) (string, bool) {
|
|
||||||
trimmed := strings.TrimSpace(localPath)
|
|
||||||
if trimmed == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(trimmed) {
|
|
||||||
normalized, err := pathsafe.NormalizeRelativeDestination(filepath.ToSlash(trimmed))
|
|
||||||
if err != nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return normalized, true
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionRoot, ok := manifestSessionRoot(previousManifest)
|
|
||||||
if !ok {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
normalized, err := pathsafe.SlashRelativeFromRoot(sessionRoot, trimmed)
|
|
||||||
if err != nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return normalized, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func manifestSessionRoot(previousManifest *manifest.Manifest) (string, bool) {
|
|
||||||
if previousManifest == nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
runRoot := filepath.Clean(strings.TrimSpace(previousManifest.LocalWorkDir))
|
|
||||||
runID := strings.TrimSpace(previousManifest.RunID)
|
|
||||||
if runRoot == "" || runID == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
if filepath.Base(runRoot) != runID {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
runsDir := filepath.Dir(runRoot)
|
|
||||||
if filepath.Base(runsDir) != config.PathRunsDirSegment {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return filepath.Dir(runsDir), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func manifestPublishedPaths(previousManifest *manifest.Manifest) []string {
|
|
||||||
if previousManifest == nil || len(previousManifest.Stages) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sr := previousManifest.Stages["publish"]
|
|
||||||
if sr == nil || sr.Metadata == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
raw, ok := sr.Metadata["published_paths"]
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
values, ok := raw.([]any)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := make([]string, 0, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
asString, ok := value.(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
normalized, err := pathsafe.NormalizeRelativeDestination(asString)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, normalized)
|
|
||||||
}
|
|
||||||
return dedupeOrderedStrings(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func relativeToSession(paths artifacts.SessionPaths, localPath string) (string, error) {
|
func relativeToSession(paths artifacts.SessionPaths, localPath string) (string, error) {
|
||||||
root := filepath.Clean(paths.Root)
|
root := filepath.Clean(paths.Root)
|
||||||
if strings.TrimSpace(root) == "" {
|
if strings.TrimSpace(root) == "" {
|
||||||
@@ -460,23 +264,3 @@ func relativeToSession(paths artifacts.SessionPaths, localPath string) (string,
|
|||||||
}
|
}
|
||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dedupeOrderedStrings(values []string) []string {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
seen := map[string]struct{}{}
|
|
||||||
out := make([]string, 0, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
trimmed := strings.TrimSpace(value)
|
|
||||||
if trimmed == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[trimmed]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[trimmed] = struct{}{}
|
|
||||||
out = append(out, trimmed)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,18 +16,17 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildPlanResolvesPublishedArtifactFromPreviousManifest(t *testing.T) {
|
func TestResolveUsesExactCommittedDeclaration(t *testing.T) {
|
||||||
cfg, paths := previousCacheTestConfig(t)
|
cfg, paths := previousCacheTestConfig(t)
|
||||||
store := &storage.FakeBackend{}
|
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")})
|
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.ListErr = context.Canceled
|
||||||
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},
|
{Name: "session_recap", Required: true},
|
||||||
}, store)
|
}, store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("BuildPlan() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
if plan.PreviousRunID != "previous-run" {
|
if plan.PreviousRunID != "previous-run" {
|
||||||
t.Fatalf("PreviousRunID = %q, want previous-run", plan.PreviousRunID)
|
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, ",") {
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
||||||
t.Fatalf("record rel paths = %#v, want %#v", got, 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, paths := previousCacheTestConfig(t)
|
||||||
|
cfg.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{Enabled: true, OutputPath: "configured/ignored.md"}
|
||||||
store := &storage.FakeBackend{}
|
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")})
|
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)
|
plan, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
|
||||||
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},
|
{Name: "session_recap", Required: true},
|
||||||
}, store)
|
}, store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("BuildPlan() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
if len(plan.Records) != 2 {
|
if len(plan.Records) != 2 {
|
||||||
t.Fatalf("records len = %d, want 2", len(plan.Records))
|
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) {
|
func TestResolveUsesCommittedSourceMappingForCustomDestination(t *testing.T) {
|
||||||
cfg, paths := previousCacheTestConfig(t)
|
cfg, paths := previousCacheTestConfig(t)
|
||||||
store := &storage.FakeBackend{}
|
store := &storage.FakeBackend{}
|
||||||
@@ -86,8 +99,8 @@ func TestResolveUsesCommittedSourceMappingForCustomDestination(t *testing.T) {
|
|||||||
if artifact.RemoteKey != customKey {
|
if artifact.RemoteKey != customKey {
|
||||||
t.Fatalf("remote key = %q, want exact configured source destination %q", 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" {
|
if artifact.LocalRelativePath != "previous/artifacts/history/recap-v2.txt" {
|
||||||
t.Fatalf("local relative path = %q, want source-local cache path", artifact.LocalRelativePath)
|
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)
|
_, 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)
|
t.Fatalf("Resolve() error = %v, want source-mapping absence", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveRejectsUndeclaredPublishedCandidates(t *testing.T) {
|
func TestResolveRejectsUndeclaredPublishedCandidatesWithoutProbing(t *testing.T) {
|
||||||
cfg, paths := previousCacheTestConfig(t)
|
cfg, paths := previousCacheTestConfig(t)
|
||||||
store := &storage.FakeBackend{}
|
store := &storage.FakeBackend{}
|
||||||
seedPreviousCurrent(t, store, cfg, previousManifestWithOutput(t, cfg, "artifacts/session_recap.md", []string{"archive/session_recap.md"}))
|
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")})
|
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)
|
_, 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)
|
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)
|
cfg, paths := previousCacheTestConfig(t)
|
||||||
store := &storage.FakeBackend{}
|
store := &storage.FakeBackend{}
|
||||||
seedPreviousCurrent(t, store, cfg, previousManifestWithOutput(t, cfg, "", nil))
|
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},
|
{Name: "session_recap", Required: false},
|
||||||
}, store)
|
}, store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("BuildPlan() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
if strings.Join(plan.SkippedMissing, ",") != "session_recap" {
|
if strings.Join(plan.SkippedMissing, ",") != "session_recap" {
|
||||||
t.Fatalf("SkippedMissing = %#v, want session_recap", plan.SkippedMissing)
|
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)
|
cfg, paths := previousCacheTestConfig(t)
|
||||||
store := &storage.FakeBackend{}
|
store := &storage.FakeBackend{}
|
||||||
seedPreviousCurrent(t, store, cfg, previousManifestWithOutput(t, cfg, "", nil))
|
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},
|
{Name: "session_recap", Required: true},
|
||||||
}, store)
|
}, 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("BuildPlan() error = %v, want required missing error", err)
|
t.Fatalf("Resolve() error = %v, want required missing error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildPlanValidatesPreviousManifestIdentity(t *testing.T) {
|
func TestResolveValidatesPreviousManifestIdentity(t *testing.T) {
|
||||||
cfg, paths := previousCacheTestConfig(t)
|
cfg, paths := previousCacheTestConfig(t)
|
||||||
store := &storage.FakeBackend{}
|
store := &storage.FakeBackend{}
|
||||||
manifest := previousManifestWithOutput(t, cfg, "artifacts/session_recap.md", nil)
|
manifest := previousManifestWithOutput(t, cfg, "artifacts/session_recap.md", nil)
|
||||||
manifest.SessionID = "wrong-session"
|
manifest.SessionID = "wrong-session"
|
||||||
seedPreviousCurrent(t, store, cfg, manifest)
|
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},
|
{Name: "session_recap", Required: true},
|
||||||
}, store)
|
}, store)
|
||||||
if err == nil || !strings.Contains(err.Error(), "identity does not match") {
|
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)
|
cfg, paths := previousCacheTestConfig(t)
|
||||||
store := &storage.FakeBackend{}
|
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},
|
{Name: "session_recap", Required: false},
|
||||||
}, store)
|
}, store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("BuildPlan() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
if strings.Join(plan.SkippedMissing, ",") != "session_recap" {
|
if strings.Join(plan.SkippedMissing, ",") != "session_recap" {
|
||||||
t.Fatalf("SkippedMissing = %#v, want session_recap", plan.SkippedMissing)
|
t.Fatalf("SkippedMissing = %#v, want session_recap", plan.SkippedMissing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildPlanMissingCurrentRunPointerFailsRequiredRequirements(t *testing.T) {
|
func TestResolveMissingCurrentRunPointerFailsRequiredRequirements(t *testing.T) {
|
||||||
cfg, paths := previousCacheTestConfig(t)
|
cfg, paths := previousCacheTestConfig(t)
|
||||||
store := &storage.FakeBackend{}
|
store := &storage.FakeBackend{}
|
||||||
|
|
||||||
_, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
|
_, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
|
||||||
{Name: "session_recap", Required: true},
|
{Name: "session_recap", Required: true},
|
||||||
}, store)
|
}, store)
|
||||||
if err == nil || !strings.Contains(err.Error(), `required previous-session artifacts unavailable: remote current commit pointer missing`) {
|
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)
|
cfg, paths := previousCacheTestConfig(t)
|
||||||
store := &storage.FakeBackend{}
|
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},
|
{Name: "session_recap", Required: false},
|
||||||
}, store)
|
}, store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("BuildPlan() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
if strings.Join(plan.SkippedMissing, ",") != "session_recap" {
|
if strings.Join(plan.SkippedMissing, ",") != "session_recap" {
|
||||||
t.Fatalf("SkippedMissing = %#v, want session_recap", plan.SkippedMissing)
|
t.Fatalf("SkippedMissing = %#v, want session_recap", plan.SkippedMissing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildPlanMissingCurrentCommitPointerFailsRequiredRequirements(t *testing.T) {
|
func TestResolveMissingCurrentCommitPointerFailsRequiredRequirements(t *testing.T) {
|
||||||
cfg, paths := previousCacheTestConfig(t)
|
cfg, paths := previousCacheTestConfig(t)
|
||||||
store := &storage.FakeBackend{}
|
store := &storage.FakeBackend{}
|
||||||
|
|
||||||
_, err := BuildPlan(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
|
_, err := Resolve(context.Background(), cfg, paths, []artifacts.PreviousArtifactRequirement{
|
||||||
{Name: "session_recap", Required: true},
|
{Name: "session_recap", Required: true},
|
||||||
}, store)
|
}, store)
|
||||||
if err == nil || !strings.Contains(err.Error(), `required previous-session artifacts unavailable: remote current commit pointer missing`) {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ func TestHydratePreviousSessionArtifactsDoesNotUseLocalPreviousWorkspaceState(t
|
|||||||
writeFile(t, filepath.Join(seed.PreviousSessionRoot, "artifacts", "session_recap.md"), "# local stale recap\n")
|
writeFile(t, filepath.Join(seed.PreviousSessionRoot, "artifacts", "session_recap.md"), "# local stale recap\n")
|
||||||
|
|
||||||
_, err := hydratePreviousSessionArtifacts(context.Background(), env, sessionPaths, requirements)
|
_, err := hydratePreviousSessionArtifacts(context.Background(), env, sessionPaths, requirements)
|
||||||
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("error = %v, want remote-object-missing failure", err)
|
t.Fatalf("error = %v, want remote-object-missing failure", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,7 +290,7 @@ func seedPreviousCurrentState(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
artifactKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(previousSessionPrefix, previousRunID), "artifacts/session_recap.md")
|
artifactKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(previousSessionPrefix, previousRunID), "published/artifacts/session_recap.md")
|
||||||
if options.includeArtifactObject {
|
if options.includeArtifactObject {
|
||||||
body := options.artifactBody
|
body := options.artifactBody
|
||||||
if body == "" {
|
if body == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user