Recheck bounded prerequisites under lock

This commit is contained in:
2026-08-29 20:42:51 +00:00
parent 2176b4371d
commit a9c5e4ad4e
2 changed files with 61 additions and 0 deletions

View File

@@ -116,6 +116,31 @@ func TestExecuteStagesRejectsBoundedPrerequisitesBeforePersistentMutation(t *tes
}
}
func TestExecuteStagesRechecksBoundedPrerequisitesUnderSessionLock(t *testing.T) {
cfg := testConfig(t)
plan := mustBoundedPlan(t, "render", "render")
m := manifest.New(cfg.Session.SessionID, time.Now().UTC())
m.Campaign = cfg.Session.Campaign
markPrefixSucceeded(m, plan)
saveBoundedManifest(t, cfg, m)
store := &prerequisiteChangingStore{local: &manifest.LocalStore{}}
runs := 0
_, err := executeStages(context.Background(), cfg, []stage.Stage{countingStage{name: "render", runs: &runs}}, RunOptions{
Plan: plan,
Env: &Env{ManifestStore: store},
})
if err == nil || !strings.Contains(err.Error(), `stage "prepare" has unusable status "running"`) {
t.Fatalf("executeStages() error = %v, want changed prerequisite rejection", err)
}
if store.loads != 2 {
t.Fatalf("manifest loads = %d, want preflight and locked reload", store.loads)
}
if runs != 0 || store.creates != 0 || store.saves != 0 {
t.Fatalf("runs=%d manifest creates=%d saves=%d, want no run or manifest mutation", runs, store.creates, store.saves)
}
}
func TestExecuteStagesBoundedCompositionUsesOnlySelectedCollaborators(t *testing.T) {
for _, test := range []struct {
name string
@@ -246,6 +271,35 @@ type prerequisiteMutationSpy struct {
saves int
}
type prerequisiteChangingStore struct {
local *manifest.LocalStore
loads int
creates int
saves int
}
func (s *prerequisiteChangingStore) Create(ctx context.Context, sessionID string) (*manifest.Manifest, error) {
s.creates++
return s.local.Create(ctx, sessionID)
}
func (s *prerequisiteChangingStore) Load(ctx context.Context, path string) (*manifest.Manifest, error) {
s.loads++
m, err := s.local.Load(ctx, path)
if err != nil {
return nil, err
}
if s.loads == 2 {
m.MarkStageRunning("prepare", time.Now().UTC())
}
return m, nil
}
func (s *prerequisiteChangingStore) Save(ctx context.Context, path string, m *manifest.Manifest) error {
s.saves++
return s.local.Save(ctx, path, m)
}
func (s *prerequisiteMutationSpy) Create(ctx context.Context, sessionID string) (*manifest.Manifest, error) {
s.creates++
return s.local.Create(ctx, sessionID)

View File

@@ -139,6 +139,13 @@ func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage
return nil, fmt.Errorf("create manifest: %w", err)
}
}
// The preflight prerequisite inspection avoids creating run state for an
// already-invalid request. Recheck the manifest protected by the session
// lock because another invocation may have changed prerequisite state while
// this invocation waited to acquire the lock.
if err := validateBoundedPrerequisites(opts.Plan, m); err != nil {
return nil, fmt.Errorf("validate bounded run prerequisites under session lock: %w", err)
}
identity.applyToSessionManifest(m)
if err := env.ManifestStore.Save(ctx, manifestPath, m); err != nil {
return nil, fmt.Errorf("save manifest identity %q: %w", manifestPath, err)