Complete compatibility purge audit
This commit is contained in:
@@ -140,7 +140,8 @@ Extraction sources (`narratio.extraction.*`):
|
||||
|
||||
Previous-session sources (`narratio.previous_session.artifact.*`):
|
||||
|
||||
- resolve only from local `previous/` cache state;
|
||||
- resolve only from one matching `previous_artifact` input record in the
|
||||
current manifest, including its exact source ID and checksum;
|
||||
- are hydrated only from the unique `published_output` declaration for their
|
||||
canonical source in the selected remote commit; and
|
||||
- never infer a path from a previous manifest, current configuration, basename,
|
||||
@@ -202,10 +203,9 @@ Core helpers:
|
||||
- `ValidateCurrentStateIdentity`
|
||||
- `RemoteCommitManifest` and `CurrentCommitPointer`
|
||||
|
||||
Typed missing-state errors:
|
||||
Typed missing-state error:
|
||||
|
||||
- `CurrentRunPointerMissingError` (`ErrCurrentRunPointerMissing`)
|
||||
- `CurrentManifestMissingError` (`ErrCurrentManifestMissing`)
|
||||
- `CurrentCommitPointerMissingError` (`ErrCurrentCommitPointerMissing`)
|
||||
|
||||
Identity validation supports caller-provided expectations:
|
||||
|
||||
@@ -262,7 +262,6 @@ physical layout.
|
||||
`internal/artifacts/prepared_input.go`
|
||||
- Current state: `internal/artifacts/current_state.go`,
|
||||
`internal/artifacts/current_state_commit.go`,
|
||||
`internal/artifacts/current_state_legacy.go`
|
||||
- Paths and keys: `internal/artifacts/paths.go`,
|
||||
`internal/artifacts/s3_keys.go`
|
||||
- Previous requirements: `internal/artifacts/previous_requirements.go`
|
||||
|
||||
@@ -315,7 +315,6 @@ where a durable running record can require operator interpretation.
|
||||
`internal/manifest/run_manifest.go`
|
||||
- Remote commit model and readers: `internal/artifacts/remote_commit.go`,
|
||||
`internal/artifacts/current_state_commit.go`,
|
||||
`internal/artifacts/current_state_legacy.go`
|
||||
- Persistence and validation: `internal/manifest/store.go`
|
||||
- Package tests: `internal/manifest/*_test.go`
|
||||
- Assembled execution behavior: `internal/app/runner_test.go`,
|
||||
|
||||
@@ -15,7 +15,7 @@ Normalize polished transcript into final transcript using Seriatim.
|
||||
|
||||
## Key Behavior
|
||||
|
||||
- resolves polished transcript from manifest outputs/canonical fallback.
|
||||
- resolves polished transcript only from verified manifest output evidence.
|
||||
- applies `pipeline.normalize` config or default normalize config.
|
||||
- runs Seriatim normalize with configured timeout/binary.
|
||||
- validates normalized transcript and optional report.
|
||||
|
||||
@@ -16,7 +16,7 @@ Run Audita polishing on base transcript and produce polished transcript.
|
||||
|
||||
## Key Behavior
|
||||
|
||||
- resolves base transcript from merge outputs/canonical fallback.
|
||||
- resolves base transcript only from verified merge manifest output evidence.
|
||||
- invokes an Audita runner configured with static model/runtime options; the
|
||||
invocation supplies paths and modules.
|
||||
- validates processed transcript structure (`segments` array required).
|
||||
|
||||
@@ -21,7 +21,7 @@ records that may consume rendered transcripts.
|
||||
## Key Behavior
|
||||
|
||||
- uses `pipeline.render` settings (enabled/format/title/booleans).
|
||||
- resolves inputs manifest-first, then canonical fallback.
|
||||
- resolves inputs only from verified manifest output evidence.
|
||||
- writes run-local outputs first, then materializes canonical session outputs.
|
||||
- records input provenance, output paths, adapter metadata, logs, and generated config refs.
|
||||
- when `pipeline.render.enabled=false`, completes successfully with no outputs
|
||||
|
||||
@@ -373,7 +373,7 @@ consumers require current manifest evidence.
|
||||
|
||||
## Stage 7 — Complete The Purge And Validate The Repository
|
||||
|
||||
**Status: Pending**
|
||||
**Status: Completed**
|
||||
|
||||
### Goal
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ keys remain protected without treating ordinary D&D data as private.
|
||||
|
||||
### Workstream 3 — Canonical contracts and compatibility purge
|
||||
|
||||
**Status:** Pending
|
||||
**Status:** Complete
|
||||
|
||||
**Audit sources:** H43 and C01.
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
|
||||
func TestDownloadObjectToTempSuccess(t *testing.T) {
|
||||
store := &FakeBackend{}
|
||||
store.SeedObject(FakeObject{Key: "sessions/a/current/run_id.txt", Data: []byte("run-123\n")})
|
||||
store.SeedObject(FakeObject{Key: "sessions/a/runs/run-123/metadata.txt", Data: []byte("run-123\n")})
|
||||
|
||||
path, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/current/run_id.txt", "narratio-test-*.txt")
|
||||
path, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/runs/run-123/metadata.txt", "narratio-test-*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadObjectToTemp() error = %v", err)
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func TestDownloadObjectToTempFailedDownloadRemovesTempFile(t *testing.T) {
|
||||
t.Fatalf("Glob(before) error = %v", err)
|
||||
}
|
||||
|
||||
path, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/current/run_id.txt", pattern)
|
||||
path, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/runs/run-123/metadata.txt", pattern)
|
||||
if !errors.Is(err, sentinel) {
|
||||
t.Fatalf("DownloadObjectToTemp() error = %v, want %v", err, sentinel)
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func TestDownloadObjectToTempCallerContextWrappingPreservesCause(t *testing.T) {
|
||||
sentinel := errors.New("object missing")
|
||||
store := &FakeBackend{DownloadErr: sentinel}
|
||||
|
||||
_, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/current/run_id.txt", "narratio-test-*.txt")
|
||||
_, err := DownloadObjectToTemp(context.Background(), store, "sessions/a/runs/run-123/metadata.txt", "narratio-test-*.txt")
|
||||
if err == nil {
|
||||
t.Fatal("DownloadObjectToTemp() error = nil, want error")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
@@ -29,3 +30,14 @@ func seedCurrentBuiltInEvidence(t *testing.T, manifestPath, sessionID, campaign,
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func setCurrentPreviousArtifactInput(t *testing.T, m *manifest.Manifest, path string) {
|
||||
t.Helper()
|
||||
checksum, err := artifacts.SHA256File(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Inputs = append(m.Inputs, manifest.InputRecord{
|
||||
Kind: "previous_artifact", Source: artifactpolicy.PreviousSessionSourceID("session_recap"), Path: path, Checksum: checksum,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) {
|
||||
{
|
||||
Kind: RestoreActionDownload,
|
||||
LocalRelativePath: "manifest.json",
|
||||
RemoteKey: "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/manifest.json",
|
||||
RemoteKey: "dnd/campaigns/sample-campaign/sessions/2026-05-03/runs/20260519T010203Z-a1b2c3d4/session-manifest.json",
|
||||
Reason: "local file missing",
|
||||
},
|
||||
},
|
||||
@@ -165,7 +165,7 @@ func TestExecuteRestoreDiscoveryErrorSurfaced(t *testing.T) {
|
||||
return &storage.FakeBackend{}, nil
|
||||
}
|
||||
discoverRemoteCurrentStateFn = func(context.Context, *config.Config, storage.ObjectStore) (*RemoteCurrentState, error) {
|
||||
return nil, fmt.Errorf("remote current run pointer missing: %q", "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/run_id.txt")
|
||||
return nil, fmt.Errorf("remote current commit pointer missing: %q", "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/commit-pointer.json")
|
||||
}
|
||||
|
||||
workspaceRoot := t.TempDir()
|
||||
@@ -177,7 +177,7 @@ func TestExecuteRestoreDiscoveryErrorSurfaced(t *testing.T) {
|
||||
if code == 0 {
|
||||
t.Fatal("exit code = 0, want non-zero")
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "remote current run pointer missing") {
|
||||
if !strings.Contains(stderr.String(), "remote current commit pointer missing") {
|
||||
t.Fatalf("stderr = %q, want discovery error context", stderr.String())
|
||||
}
|
||||
}
|
||||
@@ -250,7 +250,7 @@ func TestExecuteRestoreLoadsSecretsBeforeObjectStoreInit(t *testing.T) {
|
||||
{
|
||||
Kind: RestoreActionDownload,
|
||||
LocalRelativePath: "manifest.json",
|
||||
RemoteKey: "dnd/campaigns/sample-campaign/sessions/2026-05-03/current/manifest.json",
|
||||
RemoteKey: "dnd/campaigns/sample-campaign/sessions/2026-05-03/runs/20260519T010203Z-a1b2c3d4/session-manifest.json",
|
||||
Reason: "local file missing",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -211,6 +211,7 @@ previous_session_id: 2026-04-26
|
||||
t.Fatalf("load restored manifest: %v", err)
|
||||
}
|
||||
setCurrentBuiltInEvidence(t, localManifest, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"))
|
||||
setCurrentPreviousArtifactInput(t, localManifest, filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md"))
|
||||
if err := (&manifest.LocalStore{}).Save(context.Background(), filepath.Join(sessionRoot, "manifest.json"), localManifest); err != nil {
|
||||
t.Fatalf("save manifest evidence: %v", err)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ const (
|
||||
ArtifactBoundsSession = "narratio.bounds.session"
|
||||
|
||||
ArtifactProvenancePreviousCacheManifestInput = "manifest.inputs.previous_cache"
|
||||
ArtifactProvenancePreviousCacheFilesystem = "current_session.previous_cache"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -264,54 +263,49 @@ func ResolvePreviousSessionArtifactWithCatalog(
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("previous-session artifact source %q requires runtime artifact catalog", source)
|
||||
}
|
||||
|
||||
configuredSourceID := ConfiguredArtifactSourceID(artifactName)
|
||||
entry, ok := catalog.Lookup(configuredSourceID)
|
||||
if !ok {
|
||||
if _, ok := catalog.Lookup(ConfiguredArtifactSourceID(artifactName)); !ok {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("unsupported previous-session artifact source %q", source)
|
||||
}
|
||||
candidates := previousSessionCacheCandidatePaths(paths, entry.CanonicalRelPath)
|
||||
if len(candidates) == 0 {
|
||||
if m == nil {
|
||||
return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source}
|
||||
}
|
||||
|
||||
manifestInputPaths := manifestInputPathSet(paths, m)
|
||||
fallback := ""
|
||||
for _, candidate := range candidates {
|
||||
exists, isDir, statErr := pathExists(candidate)
|
||||
if statErr != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("stat %q: %w", candidate, statErr)
|
||||
}
|
||||
if !exists || isDir {
|
||||
var selected manifest.InputRecord
|
||||
count := 0
|
||||
for _, input := range m.Inputs {
|
||||
if input.Kind != "previous_artifact" || input.Source != source {
|
||||
continue
|
||||
}
|
||||
if err := validateResolvedContent(candidate, contentText); err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: %w", source, err)
|
||||
candidate := filepath.Clean(ResolveSessionLocalPathForRead(paths, input.Path))
|
||||
if strings.TrimSpace(candidate) == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := manifestInputPaths[candidate]; ok {
|
||||
return ResolvedSessionArtifact{
|
||||
ID: source,
|
||||
Path: candidate,
|
||||
ProducerStage: "prepare",
|
||||
OutputKind: "previous_session_cache",
|
||||
Provenance: ArtifactProvenancePreviousCacheManifestInput,
|
||||
}, nil
|
||||
}
|
||||
if fallback == "" {
|
||||
fallback = candidate
|
||||
if _, err := pathsafe.SlashRelativeFromRoot(paths.Root, candidate); err != nil {
|
||||
continue
|
||||
}
|
||||
selected, count = input, count+1
|
||||
}
|
||||
|
||||
if fallback != "" {
|
||||
return ResolvedSessionArtifact{
|
||||
ID: source,
|
||||
Path: fallback,
|
||||
ProducerStage: "prepare",
|
||||
OutputKind: "previous_session_cache",
|
||||
Provenance: ArtifactProvenancePreviousCacheFilesystem,
|
||||
}, nil
|
||||
if count != 1 {
|
||||
return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source}
|
||||
}
|
||||
|
||||
return ResolvedSessionArtifact{}, &SessionArtifactNotFoundError{ArtifactID: source}
|
||||
path := filepath.Clean(ResolveSessionLocalPathForRead(paths, selected.Path))
|
||||
if err := requireSafeSessionRegularFile(paths.Root, path); err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: unsafe previous-session artifact: %w", source, err)
|
||||
}
|
||||
if err := validateResolvedContent(path, contentText); err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: %w", source, err)
|
||||
}
|
||||
checksum := strings.TrimSpace(selected.Checksum)
|
||||
if len(checksum) != 64 || strings.ToLower(checksum) != checksum {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: previous-session artifact checksum is invalid", source)
|
||||
}
|
||||
actualChecksum, err := SHA256File(path)
|
||||
if err != nil {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("checksum %q: %w", source, err)
|
||||
}
|
||||
if actualChecksum != checksum {
|
||||
return ResolvedSessionArtifact{}, fmt.Errorf("validate %q: checksum mismatch", source)
|
||||
}
|
||||
return ResolvedSessionArtifact{ID: source, Path: path, ProducerStage: "prepare", OutputKind: "previous_session_cache", Provenance: ArtifactProvenancePreviousCacheManifestInput, Checksum: checksum}, nil
|
||||
}
|
||||
|
||||
func manifestArtifactCandidate(paths SessionPaths, m *manifest.Manifest, spec artifactSpec) (ResolvedSessionArtifact, error) {
|
||||
@@ -384,65 +378,6 @@ func requireSafeSessionRegularFile(root, path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func previousSessionCacheCandidatePaths(paths SessionPaths, canonicalRelPath string) []string {
|
||||
trimmed := strings.TrimSpace(canonicalRelPath)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
normalized := filepath.ToSlash(filepath.Clean(filepath.FromSlash(trimmed)))
|
||||
if normalized == "." || normalized == "" || normalized == ".." || strings.HasPrefix(normalized, "../") || strings.HasPrefix(normalized, "/") {
|
||||
return nil
|
||||
}
|
||||
|
||||
relCandidates := []string{normalized}
|
||||
const artifactsPrefix = "artifacts/"
|
||||
if strings.HasPrefix(normalized, artifactsPrefix) && len(normalized) > len(artifactsPrefix) {
|
||||
relCandidates = append(relCandidates, strings.TrimPrefix(normalized, artifactsPrefix))
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(relCandidates))
|
||||
seen := map[string]struct{}{}
|
||||
for _, rel := range relCandidates {
|
||||
abs, err := SessionPreviousArtifactPath(paths, rel)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
abs = filepath.Clean(abs)
|
||||
if _, ok := seen[abs]; ok {
|
||||
continue
|
||||
}
|
||||
seen[abs] = struct{}{}
|
||||
out = append(out, abs)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func manifestInputPathSet(paths SessionPaths, m *manifest.Manifest) map[string]struct{} {
|
||||
if m == nil || len(m.Inputs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]struct{}, len(m.Inputs))
|
||||
for _, in := range m.Inputs {
|
||||
resolved := filepath.Clean(ResolveSessionLocalPathForRead(paths, in.Path))
|
||||
if strings.TrimSpace(resolved) == "" {
|
||||
continue
|
||||
}
|
||||
out[resolved] = struct{}{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func pathExists(path string) (exists bool, isDir bool, err error) {
|
||||
info, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, info.IsDir(), nil
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, false, nil
|
||||
}
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
func validateResolvedContent(path string, kind artifactContentKind) error {
|
||||
switch kind {
|
||||
case contentTranscriptJSON:
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
@@ -474,9 +474,13 @@ func TestResolvePreviousSessionArtifactWithCatalogPrefersManifestInputRecord(t *
|
||||
}
|
||||
|
||||
m := manifest.New("session", time.Now().UTC())
|
||||
m.Inputs = []manifest.InputRecord{
|
||||
{Kind: "previous_artifact", Path: manifestBackedPath},
|
||||
checksum, err := SHA256File(manifestBackedPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Inputs = []manifest.InputRecord{{
|
||||
Kind: "previous_artifact", Source: artifactpolicy.PreviousSessionSourceID("session_recap"), Path: manifestBackedPath, Checksum: checksum,
|
||||
}}
|
||||
|
||||
resolved, err := ResolvePreviousSessionArtifactWithCatalog(
|
||||
paths,
|
||||
@@ -495,7 +499,7 @@ func TestResolvePreviousSessionArtifactWithCatalogPrefersManifestInputRecord(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePreviousSessionArtifactWithCatalogFallsBackToPreparedCachePath(t *testing.T) {
|
||||
func TestResolvePreviousSessionArtifactWithCatalogRejectsIncidentalPreparedCachePath(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
paths := buildSessionPaths(workspace, "campaign", "session")
|
||||
|
||||
@@ -517,31 +521,14 @@ func TestResolvePreviousSessionArtifactWithCatalogFallsBackToPreparedCachePath(t
|
||||
t.Fatalf("RegisterConfiguredArtifacts() error = %v", err)
|
||||
}
|
||||
|
||||
resolved, err := ResolvePreviousSessionArtifactWithCatalog(
|
||||
_, err := ResolvePreviousSessionArtifactWithCatalog(
|
||||
paths,
|
||||
nil,
|
||||
"narratio.previous_session.artifact.session_recap",
|
||||
catalog,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolvePreviousSessionArtifactWithCatalog() error = %v", err)
|
||||
}
|
||||
if resolved.Path != fallbackPath {
|
||||
t.Fatalf("resolved path = %q, want %q", resolved.Path, fallbackPath)
|
||||
}
|
||||
if resolved.Provenance != ArtifactProvenancePreviousCacheFilesystem {
|
||||
t.Fatalf("provenance = %q, want %q", resolved.Provenance, ArtifactProvenancePreviousCacheFilesystem)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviousSessionCacheCandidatePathsStripsArtifactsPrefix(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
paths := buildSessionPaths(workspace, "campaign", "session")
|
||||
|
||||
got := previousSessionCacheCandidatePaths(paths, "artifacts/session_recap.md")
|
||||
want := []string{filepath.Join(paths.PreviousArtifactsDir, "session_recap.md")}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("previousSessionCacheCandidatePaths() = %#v, want %#v", got, want)
|
||||
if !errors.Is(err, ErrSessionArtifactNotFound) {
|
||||
t.Fatalf("ResolvePreviousSessionArtifactWithCatalog() error = %v, want unavailable incidental cache", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
@@ -55,3 +56,14 @@ func setCurrentBuiltInEvidence(t *testing.T, m *manifest.Manifest, stage, kind,
|
||||
ProducerRunID: stage + "-run", Checksum: checksum,
|
||||
}})
|
||||
}
|
||||
|
||||
func setCurrentPreviousArtifactInput(t *testing.T, m *manifest.Manifest, path string) {
|
||||
t.Helper()
|
||||
checksum, err := artifacts.SHA256File(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Inputs = append(m.Inputs, manifest.InputRecord{
|
||||
Kind: "previous_artifact", Source: artifactpolicy.PreviousSessionSourceID("session_recap"), Path: path, Checksum: checksum,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestResolveAnalyzeInputIdentitiesSupportsEverySourceKind(t *testing.T) {
|
||||
setup: func(t *testing.T, env *Env, m *manifest.Manifest) string {
|
||||
path := mustPreviousArtifactPath(t, sessionPathsForEnv(env, m.SessionID), "artifacts/session_recap.md")
|
||||
writeAnalyzeFile(t, path, "previous recap\n")
|
||||
m.Inputs = append(m.Inputs, manifest.InputRecord{Kind: "previous_artifact", Path: path})
|
||||
setCurrentPreviousArtifactInput(t, m, path)
|
||||
return path
|
||||
},
|
||||
wantOutputKind: "previous_session_cache",
|
||||
|
||||
@@ -392,6 +392,7 @@ func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing
|
||||
|
||||
previousRecapPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md")
|
||||
writeAnalyzeFile(t, previousRecapPath, "previous recap\n")
|
||||
setCurrentPreviousArtifactInput(t, m, previousRecapPath)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: true,
|
||||
@@ -804,10 +805,7 @@ func TestAnalyzeResolvesCanonicalPreviousSessionArtifactFromManifestInput(t *tes
|
||||
|
||||
previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md")
|
||||
writeAnalyzeFile(t, previousPath, "previous recap\n")
|
||||
m.Inputs = append(m.Inputs, manifest.InputRecord{
|
||||
Kind: "previous_artifact",
|
||||
Path: previousPath,
|
||||
})
|
||||
setCurrentPreviousArtifactInput(t, m, previousPath)
|
||||
|
||||
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{
|
||||
@@ -860,10 +858,7 @@ func TestAnalyzeRenderDebugWithCanonicalPreviousSessionInput(t *testing.T) {
|
||||
|
||||
previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md")
|
||||
writeAnalyzeFile(t, previousPath, "previous recap\n")
|
||||
m.Inputs = append(m.Inputs, manifest.InputRecord{
|
||||
Kind: "previous_artifact",
|
||||
Path: previousPath,
|
||||
})
|
||||
setCurrentPreviousArtifactInput(t, m, previousPath)
|
||||
|
||||
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{
|
||||
@@ -894,10 +889,7 @@ func TestAnalyzeDoesNotCallObjectStoreForCanonicalPreviousSessionInput(t *testin
|
||||
|
||||
previousPath := mustPreviousArtifactPath(t, paths, "artifacts/session_recap.md")
|
||||
writeAnalyzeFile(t, previousPath, "previous recap\n")
|
||||
m.Inputs = append(m.Inputs, manifest.InputRecord{
|
||||
Kind: "previous_artifact",
|
||||
Path: previousPath,
|
||||
})
|
||||
setCurrentPreviousArtifactInput(t, m, previousPath)
|
||||
|
||||
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
@@ -79,11 +80,15 @@ func hydratePreviousSessionArtifacts(
|
||||
return nil, fmt.Errorf("install previous-session object %q: %w", record.RemoteKey, err)
|
||||
}
|
||||
|
||||
source := preparePreviousInputSource
|
||||
if record.Kind == preparePreviousInputKindArtifact {
|
||||
source = artifactpolicy.PreviousSessionSourceID(record.RequirementName)
|
||||
}
|
||||
result.Inputs = append(result.Inputs, manifest.InputRecord{
|
||||
Kind: record.Kind,
|
||||
Path: record.LocalPath,
|
||||
Checksum: checksum,
|
||||
Source: preparePreviousInputSource,
|
||||
Source: source,
|
||||
S3Bucket: record.S3Bucket,
|
||||
S3Key: record.RemoteKey,
|
||||
})
|
||||
|
||||
@@ -70,8 +70,8 @@ func TestHydratePreviousSessionArtifactsDownloadsManifestAndRequiredArtifact(t *
|
||||
if artifactInput == nil {
|
||||
t.Fatalf("missing input kind %q", preparePreviousInputKindArtifact)
|
||||
}
|
||||
if artifactInput.Source != preparePreviousInputSource {
|
||||
t.Fatalf("artifact input source = %q, want %q", artifactInput.Source, preparePreviousInputSource)
|
||||
if artifactInput.Source != "narratio.previous_session.artifact.session_recap" {
|
||||
t.Fatalf("artifact input source = %q, want canonical previous source", artifactInput.Source)
|
||||
}
|
||||
if artifactInput.Path != recapPath {
|
||||
t.Fatalf("artifact input path = %q, want %q", artifactInput.Path, recapPath)
|
||||
|
||||
@@ -910,9 +910,6 @@ func TestPublishFailurePreservesPreviousCommittedSnapshotAndRetryIsDeterministic
|
||||
if !sort.StringsAreSorted(destinations) {
|
||||
t.Fatalf("retried immutable mapping is not deterministic: %v", destinations)
|
||||
}
|
||||
if _, exists := fake.Objects[m.S3SessionPrefix+"current/manifest.json"]; exists {
|
||||
t.Fatal("new publication wrote legacy current manifest")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishRejectsConflictingImmutableObject(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user