Validate published output declarations
This commit is contained in:
@@ -178,7 +178,7 @@ func remotePublishedOutputAvailabilityForCurrent(
|
||||
out[publishedOutputRemoteStateKey(source, dest)] = state + "remote=missing"
|
||||
continue
|
||||
}
|
||||
identity, err := artifacts.ParseImmutableRunObjectKey(current.SessionPrefix, declaration.DestinationKey)
|
||||
identity, err := artifacts.ParsePublishedOutputObjectKey(current.SessionPrefix, declaration.DestinationKey)
|
||||
if err != nil {
|
||||
out[publishedOutputRemoteStateKey(source, dest)] = state + "remote=error"
|
||||
continue
|
||||
|
||||
@@ -855,7 +855,7 @@ func seedOperatorCommittedOutputs(t *testing.T, fake *storage.FakeBackend, outpu
|
||||
}
|
||||
sort.Strings(sources)
|
||||
for _, source := range sources {
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, runID), outputs[source])
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, runID), "published/"+outputs[source])
|
||||
fake.SeedObject(storage.FakeObject{Key: key, Data: []byte(`{"available":true}`)})
|
||||
declarations = append(declarations, remoteRestoreArtifact(fake, artifacts.RemoteArtifactTypePublishedOutput, source, key))
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestCommittedRestoreDownloadsCarriedOutputFromDeclaredRun(t *testing.T) {
|
||||
"transcripts/full.json": []byte("carried\n"),
|
||||
})
|
||||
declaration := ¤t.Commit.Artifacts[1]
|
||||
oldKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, "20260512T010203Z-11223344"), "transcripts/full.json")
|
||||
oldKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, "20260512T010203Z-11223344"), "published/transcripts/full.json")
|
||||
fake.SeedObject(storage.FakeObject{Key: oldKey, Data: []byte("carried\n")})
|
||||
oldObject := fake.Objects[oldKey]
|
||||
declaration.DestinationKey = oldKey
|
||||
@@ -145,7 +145,7 @@ func TestCommittedStatusReportsCarriedOutputOriginAndChecksExactKey(t *testing.T
|
||||
"transcripts/full.json": []byte("carried\n"),
|
||||
})
|
||||
const originRun = "20260512T010203Z-11223344"
|
||||
oldKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, originRun), "transcripts/full.json")
|
||||
oldKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, originRun), "published/transcripts/full.json")
|
||||
fake.SeedObject(storage.FakeObject{Key: oldKey, Data: []byte("carried\n")})
|
||||
current.Commit.Artifacts[1].Source = "narratio.transcript.final_trimmed"
|
||||
current.Commit.Artifacts[1].DestinationKey = oldKey
|
||||
@@ -223,7 +223,7 @@ func TestCommittedRestoreRejectsChangedDeclaredObjectBeforeManifestInstall(t *te
|
||||
if err != nil {
|
||||
t.Fatalf("buildRestorePlan() error = %v", err)
|
||||
}
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "transcripts/full.json")
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "published/transcripts/full.json")
|
||||
fake.SeedObject(storage.FakeObject{Key: key, Data: []byte("changed bytes\n")})
|
||||
|
||||
if _, err := executeRestorePlan(context.Background(), cfg, current, plan, nil, fake); err == nil {
|
||||
@@ -245,7 +245,7 @@ func TestCommittedRestoreRejectsChangedDeclaredObjectGeneration(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("buildRestorePlan() error = %v", err)
|
||||
}
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "transcripts/full.json")
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "published/transcripts/full.json")
|
||||
fake.SeedObject(storage.FakeObject{Key: key, Data: []byte("committed bytes\n"), ETag: "replacement-generation"})
|
||||
|
||||
if _, err := executeRestorePlan(context.Background(), cfg, current, plan, nil, fake); err == nil {
|
||||
@@ -281,7 +281,7 @@ func TestCommittedRestoreRejectsChangedAudioBeforeInstall(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("buildRestorePlan() error = %v", err)
|
||||
}
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "audio/alice.flac")
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "published/audio/alice.flac")
|
||||
generation := test.generation
|
||||
if generation == "" {
|
||||
generation = fake.Objects[key].ETag
|
||||
@@ -319,7 +319,7 @@ func TestCommittedRestoreRejectsMissingDeclaredObjectBeforeManifestInstall(t *te
|
||||
if err != nil {
|
||||
t.Fatalf("buildRestorePlan() error = %v", err)
|
||||
}
|
||||
missingKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "transcripts/full.json")
|
||||
missingKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "published/transcripts/full.json")
|
||||
fake.DownloadHook = func(call storage.FakeDownloadCall) error {
|
||||
if call.Key == missingKey {
|
||||
return os.ErrNotExist
|
||||
@@ -382,9 +382,10 @@ func seedCommittedRestoreSnapshot(t *testing.T, cfg *config.Config, fake *storag
|
||||
}
|
||||
sort.Strings(paths)
|
||||
for _, relative := range paths {
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, runID), relative)
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, runID), "published/"+relative)
|
||||
fake.SeedObject(storage.FakeObject{Key: key, Data: outputs[relative]})
|
||||
artifactsByKey = append(artifactsByKey, remoteRestoreArtifact(fake, artifacts.RemoteArtifactTypePublishedOutput, "narratio.test."+strings.ReplaceAll(relative, "/", "."), key))
|
||||
sourceKey := strings.NewReplacer("/", "_", ".", "_", "-", "_").Replace(relative)
|
||||
artifactsByKey = append(artifactsByKey, remoteRestoreArtifact(fake, artifacts.RemoteArtifactTypePublishedOutput, artifacts.ConfiguredArtifactSourceID(sourceKey), key))
|
||||
}
|
||||
commit := artifacts.RemoteCommitManifest{
|
||||
FormatVersion: artifacts.RemoteCommitFormatVersion,
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestExecuteRestoreIncludeAudioUsesCacheAfterWorkspaceDeletion(t *testing.T)
|
||||
if code != 0 {
|
||||
t.Fatalf("first restore exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
committedAudioKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, "20260519T010203Z-a1b2c3d4"), "audio/alice.flac")
|
||||
committedAudioKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, "20260519T010203Z-a1b2c3d4"), "published/audio/alice.flac")
|
||||
if got := fakeDownloadCount(fake, committedAudioKey); got != 1 {
|
||||
t.Fatalf("audio downloads after first restore = %d, want 1", got)
|
||||
}
|
||||
|
||||
@@ -182,12 +182,11 @@ func restoreLocalRelativePathForCommittedArtifact(sessionPrefix string, artifact
|
||||
if artifact.Type != artifacts.RemoteArtifactTypePublishedOutput {
|
||||
return "", false, nil
|
||||
}
|
||||
identity, err := artifacts.ParseImmutableRunObjectKey(sessionPrefix, artifact.DestinationKey)
|
||||
identity, err := artifacts.ParsePublishedOutputObjectKey(sessionPrefix, artifact.DestinationKey)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
rel := strings.TrimPrefix(identity.RelativePath, "published/")
|
||||
cleanRel, err := pathsafe.NormalizeRelativeDestination(rel)
|
||||
cleanRel, err := pathsafe.NormalizeRelativeDestination(identity.Destination)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("committed destination is unsafe: %w", err)
|
||||
}
|
||||
|
||||
@@ -353,14 +353,15 @@ func TestRestorePlanTraversalUnsafeKeyFails(t *testing.T) {
|
||||
func seedCurrentRestoreObject(t *testing.T, store *storage.FakeBackend, current *RemoteCurrentState, key string, data []byte) {
|
||||
t.Helper()
|
||||
artifactType := artifacts.RemoteArtifactTypePublishedOutput
|
||||
source := "narratio.restore." + strings.ReplaceAll(strings.TrimPrefix(key, current.SessionPrefix), "/", ".")
|
||||
sourceKey := strings.NewReplacer("/", "_", ".", "_", "-", "_").Replace(strings.TrimPrefix(key, current.SessionPrefix))
|
||||
source := artifacts.ConfiguredArtifactSourceID(sourceKey)
|
||||
destination := key
|
||||
if key == current.CurrentManifestKey {
|
||||
artifactType = artifacts.RemoteArtifactTypeSessionManifest
|
||||
source = "session.manifest"
|
||||
current.ManifestData = append([]byte(nil), data...)
|
||||
} else if strings.HasPrefix(key, current.SessionPrefix) {
|
||||
destination = artifacts.S3RunPrefix(current.SessionPrefix, current.RunID) + strings.TrimPrefix(key, current.SessionPrefix)
|
||||
destination = artifacts.S3RunPrefix(current.SessionPrefix, current.RunID) + "published/" + strings.TrimPrefix(key, current.SessionPrefix)
|
||||
}
|
||||
seedRestoreObject(store, destination, data)
|
||||
current.Commit.Artifacts = append(current.Commit.Artifacts, remoteRestoreArtifact(store, artifactType, source, destination))
|
||||
@@ -397,11 +398,11 @@ func seedRestoreObject(store *storage.FakeBackend, key string, data []byte) {
|
||||
destination := key
|
||||
if strings.HasPrefix(key, sessionPrefix) && !strings.HasPrefix(key, runPrefix) {
|
||||
relative := strings.TrimPrefix(key, sessionPrefix)
|
||||
destination = runPrefix + relative
|
||||
destination = runPrefix + "published/" + relative
|
||||
store.SeedObject(storage.FakeObject{Key: destination, Data: data})
|
||||
}
|
||||
typeID := artifacts.RemoteArtifactTypePublishedOutput
|
||||
source := "narratio.test"
|
||||
source := artifacts.ConfiguredArtifactSourceID("test")
|
||||
if destination == artifacts.S3RunSessionManifestKey(sessionPrefix, pointer.RunID) {
|
||||
typeID, source = artifacts.RemoteArtifactTypeSessionManifest, "session.manifest"
|
||||
} else if relative := strings.TrimPrefix(destination, runPrefix); strings.HasPrefix(relative, "artifacts/") || strings.HasPrefix(relative, "published/artifacts/") {
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestForcedRestoreRetryClearsIncompleteMarker(t *testing.T) {
|
||||
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
mustWriteTestFile(t, filepath.Join(sessionRoot, "artifacts", "session_recap.md"), "local recap\n")
|
||||
mustWriteTestFile(t, filepath.Join(sessionRoot, "transcripts", "full.json"), "local transcript\n")
|
||||
committedTranscriptKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, "20260519T010203Z-a1b2c3d4"), "transcripts/full.json")
|
||||
committedTranscriptKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, "20260519T010203Z-a1b2c3d4"), "published/transcripts/full.json")
|
||||
fake.DownloadHook = func(call storage.FakeDownloadCall) error {
|
||||
if call.Key == committedTranscriptKey {
|
||||
return context.Canceled
|
||||
|
||||
Reference in New Issue
Block a user