Complete compatibility purge audit

This commit is contained in:
2026-09-02 14:18:51 +00:00
parent 57719b7d81
commit e9c4873030
19 changed files with 99 additions and 160 deletions

View File

@@ -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`

View File

@@ -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`,

View File

@@ -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.

View File

@@ -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).

View File

@@ -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

View File

@@ -373,7 +373,7 @@ consumers require current manifest evidence.
## Stage 7 — Complete The Purge And Validate The Repository
**Status: Pending**
**Status: Completed**
### Goal

View File

@@ -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.

View File

@@ -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")
}

View File

@@ -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,
})
}

View File

@@ -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",
},
},

View File

@@ -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)
}

View File

@@ -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:

View File

@@ -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)
}
}

View File

@@ -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,
})
}

View File

@@ -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",

View File

@@ -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{

View File

@@ -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,
})

View File

@@ -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)

View File

@@ -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) {