Complete runner lifecycle refactor

This commit is contained in:
2026-09-04 01:21:37 +00:00
parent dbda2a0161
commit 2e69c14898
18 changed files with 176 additions and 165 deletions

View File

@@ -38,11 +38,7 @@ func TestExecuteRunStagePublishPropagatesSelectedArtifacts(t *testing.T) {
var capturedStages []string
var capturedArtifacts []string
origExecuteStagesFn := executeStagesFn
t.Cleanup(func() {
executeStagesFn = origExecuteStagesFn
})
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
for _, s := range plan.Stages() {
capturedStages = append(capturedStages, s.Name())
}
@@ -52,7 +48,7 @@ func TestExecuteRunStagePublishPropagatesSelectedArtifacts(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute(
code := executeWithPipelineExecutor(
[]string{
"run-stage", "publish", "2026-05-03",
"--config", pipelinePath,
@@ -62,6 +58,7 @@ func TestExecuteRunStagePublishPropagatesSelectedArtifacts(t *testing.T) {
},
&stdout,
&stderr,
executor,
)
if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
@@ -165,11 +162,7 @@ func TestExecuteAnalyzeForceRunsAnalyze(t *testing.T) {
var capturedStages []string
var capturedForce bool
origExecuteStagesFn := executeStagesFn
t.Cleanup(func() {
executeStagesFn = origExecuteStagesFn
})
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
for _, s := range plan.Stages() {
capturedStages = append(capturedStages, s.Name())
}
@@ -182,10 +175,11 @@ func TestExecuteAnalyzeForceRunsAnalyze(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute(
code := executeWithPipelineExecutor(
[]string{"analyze", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath},
&stdout,
&stderr,
executor,
)
if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
@@ -206,18 +200,14 @@ func TestExecuteAnalyzePropagatesSelectedArtifacts(t *testing.T) {
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
var capturedArtifacts []string
origExecuteStagesFn := executeStagesFn
t.Cleanup(func() {
executeStagesFn = origExecuteStagesFn
})
executeStagesFn = func(_ context.Context, _ *config.Config, _ BoundedPlan, opts RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, _ *config.Config, _ BoundedPlan, opts RunOptions) (*RunSummary, error) {
capturedArtifacts = append([]string(nil), opts.SelectedArtifacts...)
return &RunSummary{ManifestPath: filepath.Join(workspaceRoot, "manifest.json"), Executed: []string{"analyze"}}, nil
}
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute(
code := executeWithPipelineExecutor(
[]string{
"analyze",
"2026-05-03",
@@ -228,6 +218,7 @@ func TestExecuteAnalyzePropagatesSelectedArtifacts(t *testing.T) {
},
&stdout,
&stderr,
executor,
)
if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
@@ -299,11 +290,7 @@ func TestExecutePublishForceRunsPublish(t *testing.T) {
var capturedStages []string
var capturedForce bool
var capturedArtifacts []string
origExecuteStagesFn := executeStagesFn
t.Cleanup(func() {
executeStagesFn = origExecuteStagesFn
})
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
for _, s := range plan.Stages() {
capturedStages = append(capturedStages, s.Name())
}
@@ -317,10 +304,11 @@ func TestExecutePublishForceRunsPublish(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute(
code := executeWithPipelineExecutor(
[]string{"publish", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--artifacts", "session_recap"},
&stdout,
&stderr,
executor,
)
if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())

View File

@@ -48,9 +48,7 @@ func TestAssembledCanonicalAndAliasArtifactRegenerationRequestsMatch(t *testing.
force bool
}
var captured []capturedRequest
original := executeStagesFn
t.Cleanup(func() { executeStagesFn = original })
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
captured = append(captured, capturedRequest{
stages: plan.Names(), artifacts: append([]string(nil), options.SelectedArtifacts...), force: options.Force,
})
@@ -60,11 +58,11 @@ func TestAssembledCanonicalAndAliasArtifactRegenerationRequestsMatch(t *testing.
"2026-05-03", "--force", "--from", "extract", "--through", "analyze",
"--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath,
}
if err := Run(context.Background(), base, &bytes.Buffer{}); err != nil {
if err := runWithPipelineExecutor(context.Background(), base, &bytes.Buffer{}, executor); err != nil {
t.Fatalf("canonical unselected Run() error = %v", err)
}
selected := append(append([]string(nil), base...), "--artifacts", "session_recap")
if err := Run(context.Background(), selected, &bytes.Buffer{}); err != nil {
if err := runWithPipelineExecutor(context.Background(), selected, &bytes.Buffer{}, executor); err != nil {
t.Fatalf("canonical selected Run() error = %v", err)
}
var stdout, stderr bytes.Buffer
@@ -72,7 +70,7 @@ func TestAssembledCanonicalAndAliasArtifactRegenerationRequestsMatch(t *testing.
"regenerate-artifacts", "2026-05-03", "--artifacts", "session_recap",
"--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath,
}
if code := Execute(alias, &stdout, &stderr); code != 0 {
if code := executeWithPipelineExecutor(alias, &stdout, &stderr, executor); code != 0 {
t.Fatalf("alias exit=%d stderr=%q", code, stderr.String())
}
if len(captured) != 3 {
@@ -289,12 +287,10 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
t.Fatalf("plan output = %q, want testing provenance", planned.String())
}
original := executeStagesFn
t.Cleanup(func() { executeStagesFn = original })
var capturedCfg *config.Config
var capturedPlan BoundedPlan
var capturedOptions RunOptions
executeStagesFn = func(_ context.Context, cfg *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, cfg *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
capturedCfg = cfg
capturedPlan = plan
capturedOptions = options
@@ -302,7 +298,7 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
}
runArgs := append(append([]string(nil), common...), "--from", "analyze", "--through", "analyze", "--artifacts", "character_items_arannis")
var runOut bytes.Buffer
if err := Run(context.Background(), runArgs, &runOut); err != nil {
if err := runWithPipelineExecutor(context.Background(), runArgs, &runOut, executor); err != nil {
t.Fatalf("bounded family run: %v", err)
}
if !reflect.DeepEqual(capturedPlan.Names(), []string{"analyze"}) {

View File

@@ -140,9 +140,7 @@ func TestRunPassesBoundedPlanToRunner(t *testing.T) {
var capturedStages []string
var capturedPlan BoundedPlan
var capturedOptions RunOptions
original := executeStagesFn
t.Cleanup(func() { executeStagesFn = original })
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
capturedStages = plan.Names()
capturedPlan = plan
capturedOptions = options
@@ -150,7 +148,7 @@ func TestRunPassesBoundedPlanToRunner(t *testing.T) {
}
var out bytes.Buffer
err := Run(context.Background(), []string{
err := runWithPipelineExecutor(context.Background(), []string{
"2026-05-03",
"--from", "render",
"--through", "extract",
@@ -158,7 +156,7 @@ func TestRunPassesBoundedPlanToRunner(t *testing.T) {
"--config", pipelinePath,
"--campaign-file", campaignPath,
"--session", sessionPath,
}, &out)
}, &out, executor)
if err != nil {
t.Fatalf("Run() error = %v", err)
}

View File

@@ -9,10 +9,12 @@ import (
var supportedCommands = []string{"version", "run", "regenerate-artifacts", "run-stage", "analyze", "publish", "clean", "session", "config"}
var runCommandFn = Run
// Execute dispatches CLI commands and returns a process exit code.
func Execute(args []string, stdout, stderr io.Writer) int {
return executeWithPipelineExecutor(args, stdout, stderr, executePlan)
}
func executeWithPipelineExecutor(args []string, stdout, stderr io.Writer, executor pipelineExecutor) int {
if len(args) == 0 {
printUsage(stderr)
return 1
@@ -27,15 +29,15 @@ func Execute(args []string, stdout, stderr io.Writer) int {
case "version":
err = Version(cmdArgs, stdout)
case "run":
err = runCommandFn(ctx, cmdArgs, stdout)
err = runWithPipelineExecutor(ctx, cmdArgs, stdout, executor)
case "regenerate-artifacts":
err = RegenerateArtifacts(ctx, cmdArgs, stdout)
err = regenerateArtifactsWithPipelineExecutor(ctx, cmdArgs, stdout, executor)
case "run-stage":
err = RunStage(ctx, cmdArgs, stdout)
err = runStageWithPipelineExecutor(ctx, cmdArgs, stdout, executor)
case "analyze":
err = Analyze(ctx, cmdArgs, stdout)
err = analyzeWithPipelineExecutor(ctx, cmdArgs, stdout, executor)
case "publish":
err = Publish(ctx, cmdArgs, stdout)
err = publishWithPipelineExecutor(ctx, cmdArgs, stdout, executor)
case "session":
err = Session(ctx, cmdArgs, stdout)
case "clean":

View File

@@ -16,26 +16,24 @@ func TestRunProfileSelectionFlowsThroughSharedLoader(t *testing.T) {
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
enableCommandTestProfiles(t, pipelinePath)
original := executeStagesFn
t.Cleanup(func() { executeStagesFn = original })
var language string
executeStagesFn = func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
language = cfg.Pipeline.WhisperX.Language
return &RunSummary{SessionID: cfg.Session.SessionID, ManifestPath: "manifest.json"}, nil
}
if err := Run(context.Background(), []string{
if err := runWithPipelineExecutor(context.Background(), []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--profile", "testing",
}, io.Discard); err != nil {
}, io.Discard, executor); err != nil {
t.Fatal(err)
}
if language != "fr" {
t.Fatalf("explicit profile language = %q, want fr", language)
}
if err := Run(context.Background(), []string{
if err := runWithPipelineExecutor(context.Background(), []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath,
}, io.Discard); err != nil {
}, io.Discard, executor); err != nil {
t.Fatal(err)
}
if language != "en" {
@@ -43,9 +41,9 @@ func TestRunProfileSelectionFlowsThroughSharedLoader(t *testing.T) {
}
for _, profile := range []string{"unknown", ""} {
err := Run(context.Background(), []string{
err := runWithPipelineExecutor(context.Background(), []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--profile", profile,
}, io.Discard)
}, io.Discard, executor)
if err == nil || !strings.Contains(err.Error(), "profile") {
t.Fatalf("profile %q error = %v, want selection rejection", profile, err)
}

View File

@@ -9,6 +9,10 @@ import (
// RegenerateArtifacts expands the convenience command into its canonical run
// invocation. The run command remains the sole owner of parsing and execution.
func RegenerateArtifacts(ctx context.Context, args []string, out io.Writer) error {
return regenerateArtifactsWithPipelineExecutor(ctx, args, out, executePlan)
}
func regenerateArtifactsWithPipelineExecutor(ctx context.Context, args []string, out io.Writer, executor pipelineExecutor) error {
if containsHelpOption(args) {
printRegenerateArtifactsHelp(out)
return nil
@@ -21,7 +25,7 @@ func RegenerateArtifacts(ctx context.Context, args []string, out io.Writer) erro
}
expanded = append(expanded, "--force", "--from", "extract", "--through", "analyze")
expanded = append(expanded, args...)
return runCommandFn(ctx, expanded, out)
return runWithPipelineExecutor(ctx, expanded, out, executor)
}
func containsHelpOption(args []string) bool {

View File

@@ -7,82 +7,80 @@ import (
"reflect"
"strings"
"testing"
"gitea.maximumdirect.net/eric/narratio/internal/config"
)
func TestRegenerateArtifactsForwardsExactCanonicalRunArguments(t *testing.T) {
original := runCommandFn
t.Cleanup(func() { runCommandFn = original })
var captured []string
runCommandFn = func(_ context.Context, args []string, _ io.Writer) error {
captured = append([]string(nil), args...)
return nil
func TestRegenerateArtifactsForwardsCanonicalPipelineRequest(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
enableCommandTestProfiles(t, pipelinePath)
var capturedPlan BoundedPlan
var capturedOptions RunOptions
executor := func(_ context.Context, cfg *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
capturedPlan = plan
capturedOptions = options
return &RunSummary{SessionID: cfg.Session.SessionID, ManifestPath: "manifest.json"}, nil
}
code := Execute([]string{
code := executeWithPipelineExecutor([]string{
"regenerate-artifacts", "2026-05-03",
"--artifacts", "session_recap,player_handout",
"--artifacts=player_handout",
"--config", "pipeline.yml",
"--campaign", "sample-campaign",
"--config", pipelinePath,
"--campaign-file", campaignPath,
"--session", sessionPath,
"--profile", "testing",
}, io.Discard, io.Discard)
}, io.Discard, io.Discard, executor)
if code != 0 {
t.Fatalf("Execute() code = %d, want 0", code)
t.Fatalf("executeWithPipelineExecutor() code = %d, want 0", code)
}
want := []string{
"2026-05-03", "--force", "--from", "extract", "--through", "analyze",
"--artifacts", "session_recap,player_handout",
"--artifacts=player_handout",
"--config", "pipeline.yml",
"--campaign", "sample-campaign",
"--profile", "testing",
if got, want := capturedPlan.Names(), []string{"extract", "analyze"}; !reflect.DeepEqual(got, want) {
t.Fatalf("plan = %#v, want %#v", got, want)
}
if !reflect.DeepEqual(captured, want) {
t.Fatalf("forwarded args = %#v, want %#v", captured, want)
if !capturedOptions.Force {
t.Fatal("force = false, want true")
}
if got, want := capturedOptions.SelectedArtifacts, []string{"player_handout", "session_recap"}; !reflect.DeepEqual(got, want) {
t.Fatalf("selected artifacts = %#v, want %#v", got, want)
}
}
func TestRegenerateArtifactsForwardsSessionIDCompatibilityFlag(t *testing.T) {
original := runCommandFn
t.Cleanup(func() { runCommandFn = original })
var captured []string
runCommandFn = func(_ context.Context, args []string, _ io.Writer) error {
captured = append([]string(nil), args...)
return nil
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
var capturedSessionID string
executor := func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
capturedSessionID = cfg.Session.SessionID
return &RunSummary{SessionID: cfg.Session.SessionID, ManifestPath: "manifest.json"}, nil
}
code := Execute([]string{
code := executeWithPipelineExecutor([]string{
"regenerate-artifacts",
"--config", "pipeline.yml",
"--config", pipelinePath,
"--campaign-file", campaignPath,
"--session", sessionPath,
"--session-id", "2026-05-03",
"--artifacts", "session_recap",
}, io.Discard, io.Discard)
}, io.Discard, io.Discard, executor)
if code != 0 {
t.Fatalf("Execute() code = %d, want 0", code)
t.Fatalf("executeWithPipelineExecutor() code = %d, want 0", code)
}
want := []string{
"--force", "--from", "extract", "--through", "analyze",
"--config", "pipeline.yml",
"--session-id", "2026-05-03",
"--artifacts", "session_recap",
}
if !reflect.DeepEqual(captured, want) {
t.Fatalf("forwarded args = %#v, want %#v", captured, want)
if capturedSessionID != "2026-05-03" {
t.Fatalf("session id = %q, want compatibility flag value", capturedSessionID)
}
}
func TestRegenerateArtifactsHelpDoesNotInvokeRun(t *testing.T) {
original := runCommandFn
t.Cleanup(func() { runCommandFn = original })
called := false
runCommandFn = func(_ context.Context, _ []string, _ io.Writer) error {
executor := func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error) {
called = true
return nil
return nil, nil
}
var stdout bytes.Buffer
var stderr bytes.Buffer
if code := Execute([]string{"regenerate-artifacts", "--help"}, &stdout, &stderr); code != 0 {
if code := executeWithPipelineExecutor([]string{"regenerate-artifacts", "--help"}, &stdout, &stderr, executor); code != 0 {
t.Fatalf("Execute() code = %d, stderr = %q", code, stderr.String())
}
if called {

View File

@@ -92,7 +92,7 @@ func TestRemoteSessionConfigIsRemovedAfterEveryCommandExit(t *testing.T) {
name string
sessionYAML string
command []string
configureRun func()
executor pipelineExecutor
wantSuccessful bool
}{
{
@@ -126,10 +126,8 @@ inputs:
prefix: audio/
`,
command: []string{"run", "2026-05-03"},
configureRun: func() {
executeStagesFn = func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error) {
return nil, errors.New("adapter failed")
}
executor: func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error) {
return nil, errors.New("adapter failed")
},
},
{
@@ -140,10 +138,8 @@ inputs:
prefix: audio/
`,
command: []string{"run", "2026-05-03"},
configureRun: func() {
executeStagesFn = func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error) {
return nil, context.Canceled
}
executor: func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error) {
return nil, context.Canceled
},
},
}
@@ -159,16 +155,15 @@ inputs:
var downloadedPath string
captureRemoteSessionTempPath(t, &downloadedPath)
if tt.configureRun != nil {
origExecuteStagesFn := executeStagesFn
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
tt.configureRun()
executor := tt.executor
if executor == nil {
executor = executePlan
}
args := append(append([]string(nil), tt.command...), "--config", pipelinePath, "--campaign-file", campaignPath)
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute(args, &stdout, &stderr)
code := executeWithPipelineExecutor(args, &stdout, &stderr, executor)
if tt.wantSuccessful && code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
}

View File

@@ -29,11 +29,7 @@ func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
restoreWithStoreAndRealPhases(t, fake)
origExecuteStagesFn := executeStagesFn
t.Cleanup(func() {
executeStagesFn = origExecuteStagesFn
})
executeStagesFn = func(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
executor := func(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
if opts.Env == nil {
opts.Env = &Env{}
}
@@ -81,7 +77,7 @@ func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
stdout.Reset()
stderr.Reset()
runStageCode := Execute(
runStageCode := executeWithPipelineExecutor(
[]string{
"run-stage", "analyze", cfg.Session.SessionID,
"--config", pipelinePath,
@@ -92,6 +88,7 @@ func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
},
&stdout,
&stderr,
executor,
)
if runStageCode != 0 {
t.Fatalf("run-stage exit code = %d, want 0; stderr=%q", runStageCode, stderr.String())
@@ -209,14 +206,12 @@ previous_session_id: 2026-04-26
mustReadEquals(t, filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md"), "# previous recap\n")
scriptoriumFake := &scriptorium.FakeRunner{}
origExecuteStagesFn := executeStagesFn
origObjectStoreFn := newObjectStoreFromConfigFn
objectStoreConstructed := false
t.Cleanup(func() {
executeStagesFn = origExecuteStagesFn
newObjectStoreFromConfigFn = origObjectStoreFn
})
executeStagesFn = func(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
executor := func(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
if opts.Env == nil {
opts.Env = &Env{}
}
@@ -230,7 +225,7 @@ previous_session_id: 2026-04-26
stdout.Reset()
stderr.Reset()
runStageCode := Execute(
runStageCode := executeWithPipelineExecutor(
[]string{
"run-stage", "analyze", cfg.Session.SessionID,
"--config", pipelinePath,
@@ -241,6 +236,7 @@ previous_session_id: 2026-04-26
},
&stdout,
&stderr,
executor,
)
if runStageCode != 0 {
t.Fatalf("run-stage exit code = %d, want 0; stderr=%q", runStageCode, stderr.String())

View File

@@ -12,6 +12,10 @@ import (
// Run executes the pipeline plan and persists manifest state.
func Run(ctx context.Context, args []string, out io.Writer) error {
return runWithPipelineExecutor(ctx, args, out, executePlan)
}
func runWithPipelineExecutor(ctx context.Context, args []string, out io.Writer, executor pipelineExecutor) error {
request, err := parseBoundedRunRequest("run", args, out)
if err != nil {
if errors.Is(err, flag.ErrHelp) {
@@ -37,7 +41,7 @@ func Run(ctx context.Context, args []string, out io.Writer) error {
if err != nil {
return fmt.Errorf("run: %w", err)
}
summary, err := executeStagesFn(ctx, cfg, request.Plan, RunOptions{
summary, err := executor(ctx, cfg, request.Plan, RunOptions{
Force: request.Force,
SelectedArtifacts: selectedArtifacts,
EffectiveArtifacts: effectiveArtifacts,

View File

@@ -12,6 +12,10 @@ import (
// RunStage executes exactly one selected stage.
func RunStage(ctx context.Context, args []string, out io.Writer) error {
return runStageWithPipelineExecutor(ctx, args, out, executePlan)
}
func runStageWithPipelineExecutor(ctx context.Context, args []string, out io.Writer, executor pipelineExecutor) error {
var stageName string
var positionalSessionID string
if len(args) >= 2 && !isCLIFlagToken(args[0]) && !isCLIFlagToken(args[1]) {
@@ -74,7 +78,7 @@ func RunStage(ctx context.Context, args []string, out io.Writer) error {
PreviousSessionID: flags.previousSessionID,
Force: force,
SelectedArtifacts: normalizedArtifacts,
})
}, executor)
if err != nil {
return err
}
@@ -93,6 +97,10 @@ func RunStage(ctx context.Context, args []string, out io.Writer) error {
// Analyze force-runs the analyze stage.
func Analyze(ctx context.Context, args []string, out io.Writer) error {
return analyzeWithPipelineExecutor(ctx, args, out, executePlan)
}
func analyzeWithPipelineExecutor(ctx context.Context, args []string, out io.Writer, executor pipelineExecutor) error {
fs := flag.NewFlagSet("analyze", flag.ContinueOnError)
fs.SetOutput(io.Discard)
@@ -123,7 +131,7 @@ func Analyze(ctx context.Context, args []string, out io.Writer) error {
PreviousSessionID: flags.previousSessionID,
Force: true,
SelectedArtifacts: normalizedArtifacts,
})
}, executor)
if err != nil {
return err
}
@@ -140,6 +148,10 @@ func Analyze(ctx context.Context, args []string, out io.Writer) error {
// Publish force-runs the publish stage.
func Publish(ctx context.Context, args []string, out io.Writer) error {
return publishWithPipelineExecutor(ctx, args, out, executePlan)
}
func publishWithPipelineExecutor(ctx context.Context, args []string, out io.Writer, executor pipelineExecutor) error {
fs := flag.NewFlagSet("publish", flag.ContinueOnError)
fs.SetOutput(io.Discard)
@@ -170,7 +182,7 @@ func Publish(ctx context.Context, args []string, out io.Writer) error {
PreviousSessionID: flags.previousSessionID,
Force: true,
SelectedArtifacts: normalizedArtifacts,
})
}, executor)
if err != nil {
return err
}
@@ -198,7 +210,7 @@ type singleStageCommand struct {
SelectedArtifacts []string
}
func runSingleStageCommand(ctx context.Context, req singleStageCommand) (*RunSummary, error) {
func runSingleStageCommand(ctx context.Context, req singleStageCommand, executor pipelineExecutor) (*RunSummary, error) {
plan, err := buildSingleStageExecutionPlan(req.StageName)
if err != nil {
return nil, fmt.Errorf("%s: %w", req.CommandName, err)
@@ -224,7 +236,7 @@ func runSingleStageCommand(ctx context.Context, req singleStageCommand) (*RunSum
if err != nil {
return nil, fmt.Errorf("%s: %w", req.CommandName, err)
}
summary, err := executeStagesFn(ctx, cfg, plan, RunOptions{
summary, err := executor(ctx, cfg, plan, RunOptions{
Force: req.Force,
SelectedArtifacts: selectedArtifacts,
EffectiveArtifacts: effectiveArtifacts,

View File

@@ -34,7 +34,7 @@ type RunSummary struct {
Skipped []string
}
var executeStagesFn = executePlan
type pipelineExecutor func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error)
func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions) (summary *RunSummary, resultErr error) {
return executePlanWithDependencies(ctx, cfg, plan, opts, invocationDependencies{})

View File

@@ -18,9 +18,7 @@ func TestExecuteRunAcceptsPositionalSessionID(t *testing.T) {
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
var capturedSessionID string
origExecuteStagesFn := executeStagesFn
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
executeStagesFn = func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
capturedSessionID = cfg.Session.SessionID
return &RunSummary{
SessionID: cfg.Session.SessionID,
@@ -31,13 +29,13 @@ func TestExecuteRunAcceptsPositionalSessionID(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute([]string{
code := executeWithPipelineExecutor([]string{
"run",
"2026-05-03",
"--config", pipelinePath,
"--campaign-file", campaignPath,
"--session", sessionPath,
}, &stdout, &stderr)
}, &stdout, &stderr, executor)
if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
}
@@ -114,9 +112,7 @@ inputs:
audio_s3:
prefix: audio/
`)
origExecuteStagesFn := executeStagesFn
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
executeStagesFn = func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
return &RunSummary{
SessionID: cfg.Session.SessionID,
ManifestPath: filepath.Join(workspaceRoot, "manifest.json"),
@@ -126,12 +122,12 @@ inputs:
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute([]string{
code := executeWithPipelineExecutor([]string{
"run",
"2026-06-07",
"--config", pipelinePath,
"--campaign-file", campaignPath,
}, &stdout, &stderr)
}, &stdout, &stderr, executor)
if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
}
@@ -191,9 +187,7 @@ func TestExecuteWorkflowCommandsAcceptPositionalSessionID(t *testing.T) {
var capturedStages []string
var capturedForce bool
var capturedArtifacts []string
origExecuteStagesFn := executeStagesFn
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
for _, s := range plan.Stages() {
capturedStages = append(capturedStages, s.Name())
}
@@ -208,7 +202,7 @@ func TestExecuteWorkflowCommandsAcceptPositionalSessionID(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute(tt.args, &stdout, &stderr)
code := executeWithPipelineExecutor(tt.args, &stdout, &stderr, executor)
if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
}