Unify invocation manifest identity

This commit is contained in:
2026-08-10 19:25:03 +00:00
parent 9900211fa4
commit a1ceb457e9
4 changed files with 296 additions and 100 deletions

View File

@@ -71,6 +71,10 @@ type captureSelectedArtifactsStage struct {
captured *[]string
}
type captureConfigStage struct {
captured **config.Config
}
type captureNotariusStage struct {
captured *bool
}
@@ -108,6 +112,13 @@ func (s captureSelectedArtifactsStage) Run(_ context.Context, env *stage.Env, _
return &stage.StageResult{Metadata: map[string]any{"captured": true}}, nil
}
func (captureConfigStage) Name() string { return "prepare" }
func (captureConfigStage) Declares() stage.IODecl { return stage.IODecl{} }
func (s captureConfigStage) Run(_ context.Context, env *stage.Env, _ *manifest.Manifest) (*stage.StageResult, error) {
*s.captured = env.Config
return &stage.StageResult{}, nil
}
type analyzeOutputStage struct {
output artifacts.Ref
}
@@ -183,6 +194,24 @@ func TestExecuteStagesPropagatesSelectedArtifactsToEnv(t *testing.T) {
}
}
func TestExecuteStagesBindsResolvedConfigToEnvironment(t *testing.T) {
resolved := testConfig(t)
stale := testConfig(t)
var captured *config.Config
env := &Env{Config: stale}
_, err := executeStages(context.Background(), resolved, []stage.Stage{captureConfigStage{captured: &captured}}, RunOptions{Env: env})
if err != nil {
t.Fatalf("executeStages() error = %v", err)
}
if env.Config != resolved {
t.Fatalf("environment config = %p, want resolved config %p", env.Config, resolved)
}
if captured != resolved {
t.Fatalf("stage config = %p, want resolved config %p", captured, resolved)
}
}
func TestExecuteStagesComposesNotariusOnlyForEnabledExtraction(t *testing.T) {
cfg := testConfig(t)
cfg.Pipeline.Notarius = &config.NotariusConfig{Enabled: true}
@@ -916,10 +945,24 @@ func TestExecuteStagesLoadsExistingManifest(t *testing.T) {
func TestExecuteStagesCreatesRunManifestPerInvocation(t *testing.T) {
cfg := testConfig(t)
cfg.Pipeline.Spool.Root = t.TempDir()
cfg.Pipeline.Storage.S3 = &config.StorageS3Config{
Bucket: "archive-bucket",
RootPrefix: "narratio",
}
run1, err := executeStages(context.Background(), cfg, []stage.Stage{BuildFullPlan()[0]}, RunOptions{})
if err != nil {
t.Fatalf("first executeStages() error = %v", err)
}
store := &manifest.LocalStore{}
sessionManifest, err := store.Load(context.Background(), run1.ManifestPath)
if err != nil {
t.Fatalf("Load first session manifest error = %v", err)
}
sessionManifest.Inputs = append(sessionManifest.Inputs, manifest.InputRecord{Kind: "audio", Path: "audio/alice.flac"})
if err := store.Save(context.Background(), run1.ManifestPath, sessionManifest); err != nil {
t.Fatalf("Save session history error = %v", err)
}
run2, err := executeStages(context.Background(), cfg, []stage.Stage{BuildFullPlan()[0]}, RunOptions{Force: true})
if err != nil {
t.Fatalf("second executeStages() error = %v", err)
@@ -943,14 +986,111 @@ func TestExecuteStagesCreatesRunManifestPerInvocation(t *testing.T) {
}
}
store := &manifest.LocalStore{}
sessionManifest, err := store.Load(context.Background(), run2.ManifestPath)
sessionManifest, err = store.Load(context.Background(), run2.ManifestPath)
if err != nil {
t.Fatalf("Load session manifest error = %v", err)
}
if sessionManifest.RunID != run2.RunID {
t.Fatalf("session manifest run_id = %q, want latest run id %q", sessionManifest.RunID, run2.RunID)
}
if len(sessionManifest.Inputs) != 1 || sessionManifest.Inputs[0].Path != "audio/alice.flac" {
t.Fatalf("session history inputs = %#v, want preserved input", sessionManifest.Inputs)
}
for _, run := range []*RunSummary{run1, run2} {
runManifest, err := store.LoadRun(context.Background(), run.RunManifestPath)
if err != nil {
t.Fatalf("LoadRun(%q) error = %v", run.RunManifestPath, err)
}
wantWorkDir := artifacts.SessionRunRootForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, run.RunID)
wantSpoolDir := artifacts.SessionSpoolAudioDir(cfg.Pipeline.Spool.Root, cfg.Session.Campaign, cfg.Session.SessionID, run.RunID)
wantSessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
wantRunPrefix := artifacts.S3RunPrefix(wantSessionPrefix, run.RunID)
if runManifest.SessionID != cfg.Session.SessionID || runManifest.Campaign != cfg.Session.Campaign || runManifest.RunID != run.RunID {
t.Fatalf("run identity = (%q, %q, %q), want (%q, %q, %q)", runManifest.SessionID, runManifest.Campaign, runManifest.RunID, cfg.Session.SessionID, cfg.Session.Campaign, run.RunID)
}
if runManifest.SessionManifestPath != run.ManifestPath || runManifest.LocalWorkDir != wantWorkDir || runManifest.LocalSpoolDir != wantSpoolDir {
t.Fatalf("run paths = (%q, %q, %q), want (%q, %q, %q)", runManifest.SessionManifestPath, runManifest.LocalWorkDir, runManifest.LocalSpoolDir, run.ManifestPath, wantWorkDir, wantSpoolDir)
}
if runManifest.S3Bucket != cfg.Pipeline.Storage.S3.Bucket || runManifest.S3SessionPrefix != wantSessionPrefix || runManifest.S3RunPrefix != wantRunPrefix {
t.Fatalf("run remote identity = (%q, %q, %q), want (%q, %q, %q)", runManifest.S3Bucket, runManifest.S3SessionPrefix, runManifest.S3RunPrefix, cfg.Pipeline.Storage.S3.Bucket, wantSessionPrefix, wantRunPrefix)
}
}
if sessionManifest.LocalWorkDir == artifacts.SessionRunRootForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, run1.RunID) {
t.Fatal("session manifest retained the prior invocation work directory")
}
}
func TestExecuteStagesRejectsPersistedManifestIdentityMismatch(t *testing.T) {
for _, tc := range []struct {
name string
configure func(*manifest.Manifest)
wantDetail string
}{
{
name: "session",
configure: func(m *manifest.Manifest) {
m.SessionID = "2026-05-04"
},
wantDetail: "persisted manifest session_id",
},
{
name: "campaign",
configure: func(m *manifest.Manifest) {
m.Campaign = "other-campaign"
},
wantDetail: "persisted manifest campaign",
},
} {
t.Run(tc.name, func(t *testing.T) {
cfg := testConfig(t)
manifestPath := manifestPathFor(cfg)
store := &manifest.LocalStore{}
persisted := manifest.New(cfg.Session.SessionID, time.Now().UTC())
persisted.Campaign = cfg.Session.Campaign
tc.configure(persisted)
if err := os.MkdirAll(filepath.Dir(manifestPath), 0o755); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
if err := store.Save(context.Background(), manifestPath, persisted); err != nil {
t.Fatalf("Save manifest error = %v", err)
}
before, err := os.ReadFile(manifestPath)
if err != nil {
t.Fatalf("ReadFile() error = %v", err)
}
runs := 0
_, err = executeStages(context.Background(), cfg, []stage.Stage{countingStage{name: "prepare", runs: &runs}}, RunOptions{})
if err == nil || !strings.Contains(err.Error(), tc.wantDetail) {
t.Fatalf("executeStages() error = %v, want %q", err, tc.wantDetail)
}
if runs != 0 {
t.Fatalf("stage runs = %d, want 0", runs)
}
after, err := os.ReadFile(manifestPath)
if err != nil {
t.Fatalf("ReadFile() after rejection error = %v", err)
}
if string(after) != string(before) {
t.Fatal("persisted manifest changed after identity rejection")
}
paths := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root).SessionPathsFor(cfg.Session.Campaign, cfg.Session.SessionID)
if _, err := os.Stat(paths.LockPath); !os.IsNotExist(err) {
t.Fatalf("lock path stat error = %v, want no lock side effect", err)
}
})
}
}
func TestExecuteStagesRejectsConfiguredCampaignDisagreement(t *testing.T) {
cfg := testConfig(t)
cfg.Session.Campaign = "other-campaign"
_, err := executeStages(context.Background(), cfg, []stage.Stage{countingStage{name: "prepare", runs: new(int)}}, RunOptions{})
if err == nil || !strings.Contains(err.Error(), "configured session campaign") {
t.Fatalf("executeStages() error = %v, want configured campaign disagreement", err)
}
}
func TestExecuteStagesRunManifestRecordsSkippedStage(t *testing.T) {