Complete runner lifecycle refactor
This commit is contained in:
@@ -35,8 +35,9 @@ Adapters do not own:
|
|||||||
|
|
||||||
## Default Wiring
|
## Default Wiring
|
||||||
|
|
||||||
`internal/app/runner.go` initializes default adapters when not injected and
|
The application composes default adapters per invocation when they were not
|
||||||
only when the selected execution plan needs them:
|
injected through the stage environment, and only when the selected execution
|
||||||
|
plan needs them:
|
||||||
|
|
||||||
- WhisperX HTTP client for `transcribe`.
|
- WhisperX HTTP client for `transcribe`.
|
||||||
- Seriatim subprocess runner for `merge`, `normalize`, `trim`, or `render`.
|
- Seriatim subprocess runner for `merge`, `normalize`, `trim`, or `render`.
|
||||||
@@ -49,8 +50,10 @@ only when the selected execution plan needs them:
|
|||||||
Remote publish locks are loaded only for a selected, enabled publish that
|
Remote publish locks are loaded only for a selected, enabled publish that
|
||||||
uploads a run. Shared session lifecycle setup still applies to every selected
|
uploads a run. Shared session lifecycle setup still applies to every selected
|
||||||
range, but an unselected integration is neither initialized nor validated by
|
range, but an unselected integration is neither initialized nor validated by
|
||||||
runner composition. Each selected stage retains its own fail-fast configuration
|
application-owned composition. Constructor, clock, randomness, remote-lock,
|
||||||
and input validation.
|
and post-publish deletion boundaries are scoped to the invocation; stages still
|
||||||
|
receive their collaborators through the stage environment. Each selected stage
|
||||||
|
retains its own fail-fast configuration and input validation.
|
||||||
|
|
||||||
`session plan` is outside production adapter composition. It performs
|
`session plan` is outside production adapter composition. It performs
|
||||||
resume validation and models selected transitions against cloned manifest
|
resume validation and models selected transitions against cloned manifest
|
||||||
@@ -88,7 +91,8 @@ configured filesystem secrets before adapter initialization.
|
|||||||
|
|
||||||
## Implementation And Tests
|
## Implementation And Tests
|
||||||
|
|
||||||
- Composition: `internal/app/runner.go`, `internal/app/object_store.go`
|
- Composition: `internal/app/runner_composition.go`,
|
||||||
|
`internal/app/runner_dependencies.go`, `internal/app/object_store.go`
|
||||||
- Shared subprocess mechanics: `internal/adapters/subprocess`
|
- Shared subprocess mechanics: `internal/adapters/subprocess`
|
||||||
- Focused adapters: `internal/adapters/{whisperx,seriatim,audita,scriptorium,notarius,storage,notify}`
|
- Focused adapters: `internal/adapters/{whisperx,seriatim,audita,scriptorium,notarius,storage,notify}`
|
||||||
- `internal/adapters/whisperx/http_test.go`
|
- `internal/adapters/whisperx/http_test.go`
|
||||||
|
|||||||
@@ -155,11 +155,21 @@ durability guarantee. Callers reload after a returned save error before retrying
|
|||||||
|
|
||||||
## Execution Semantics
|
## Execution Semantics
|
||||||
|
|
||||||
The application runner marks an executing stage running and then succeeded or
|
Planning and execution share one pure pre-execution decision over the recorded
|
||||||
failed in the session manifest, persisting each transition. On success it records
|
stage status, force selection, and normalized resume-validation result. That
|
||||||
outputs, logs, generated configuration references, metadata, and—when the
|
decision identifies run versus reuse and any required stale and dependent
|
||||||
stage implements the optional contract—a versioned semantic-configuration
|
invalidation transition. Planning applies it only to a cloned manifest.
|
||||||
fingerprint. Artifact
|
Execution remains the sole owner of observed stage results and persistence.
|
||||||
|
|
||||||
|
For each selected stage, the application runner computes semantic evidence,
|
||||||
|
performs any eligible resume validation, applies the pre-execution decision,
|
||||||
|
and persists required invalidation before either reusing the existing success
|
||||||
|
or beginning an attempt. The same one-stage execution owner marks the attempt
|
||||||
|
running before invoking the stage and then records and persists its failed,
|
||||||
|
skipped, or successful terminal state before a later stage may start. On
|
||||||
|
success it records outputs, logs, generated configuration references, metadata,
|
||||||
|
and—when the stage implements the optional contract—a versioned
|
||||||
|
semantic-configuration fingerprint. Artifact
|
||||||
records may include optional contract and external provenance objects; old
|
records may include optional contract and external provenance objects; old
|
||||||
manifests remain compatible when those fields are absent. A successful forced
|
manifests remain compatible when those fields are absent. A successful forced
|
||||||
rerun marks only succeeded transitive dependent session-stage records stale.
|
rerun marks only succeeded transitive dependent session-stage records stale.
|
||||||
@@ -237,9 +247,9 @@ invalidates only stages whose semantic configuration changed. When a private
|
|||||||
external-tool model, module, prompt, or profile changes behind an unchanged
|
external-tool model, module, prompt, or profile changes behind an unchanged
|
||||||
configured identifier, use `--force` for the affected work.
|
configured identifier, use `--force` for the affected work.
|
||||||
|
|
||||||
`session plan` computes the same current fingerprint and applies the same
|
`session plan` computes the same current fingerprint and applies the shared
|
||||||
comparison and invalidation rules to a cloned manifest. It predicts the runner
|
decision and invalidation rules to a cloned manifest. It predicts the runner
|
||||||
decision without persisting session state. The shared helper
|
decision without persisting session state. The semantic fingerprint owner
|
||||||
hashes deterministic JSON from stage-owned typed structs; stage providers must
|
hashes deterministic JSON from stage-owned typed structs; stage providers must
|
||||||
exclude secrets, complete effective-configuration dumps, and operational
|
exclude secrets, complete effective-configuration dumps, and operational
|
||||||
values that cannot affect canonical results. Concrete coverage is owned by the
|
values that cannot affect canonical results. Concrete coverage is owned by the
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ session safety controls, and runs commands. Pipeline commands execute the
|
|||||||
canonical stage sequence through adapter interfaces, while manifests record
|
canonical stage sequence through adapter interfaces, while manifests record
|
||||||
progress and artifact services resolve durable inputs and outputs.
|
progress and artifact services resolve durable inputs and outputs.
|
||||||
|
|
||||||
|
The pipeline runner follows one explicit invocation lifecycle: inspect bounded
|
||||||
|
prerequisites without mutation, prepare the workspace layout, acquire one
|
||||||
|
session lock, load or create the authoritative session manifest, compose only
|
||||||
|
the collaborators needed by the selected range, execute each selected stage in
|
||||||
|
order, complete any required post-publish cleanup, and then return the summary.
|
||||||
|
The lock remains held from manifest initialization through finalization, and a
|
||||||
|
failed stage or required manifest save prevents later stages from starting.
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
| Area | Implemented owners | Responsibility |
|
| Area | Implemented owners | Responsibility |
|
||||||
@@ -44,9 +52,11 @@ adapters. The normative rules for these relationships remain in
|
|||||||
[Architecture](../policy/architecture.md).
|
[Architecture](../policy/architecture.md).
|
||||||
|
|
||||||
Pipeline execution and `session plan` share the same inclusive contiguous-range
|
Pipeline execution and `session plan` share the same inclusive contiguous-range
|
||||||
model. Planning clones session state and applies selected-stage transitions and
|
model and the same pure pre-execution decision over recorded status, force, and
|
||||||
resume validation in memory; it does not create invocation state or initialize
|
normalized resume validation. Planning applies that decision and modeled
|
||||||
stage-execution adapters. Command configuration loading can still retrieve a
|
selected-stage transitions to cloned session state; it does not persist, create
|
||||||
|
invocation state, initialize stage-execution adapters, or claim to observe an
|
||||||
|
execution result. Command configuration loading can still retrieve a
|
||||||
missing session file through configured remote storage. It retains the initially
|
missing session file through configured remote storage. It retains the initially
|
||||||
composed pipeline and selected campaign while resolving either a local or
|
composed pipeline and selected campaign while resolving either a local or
|
||||||
downloaded remote session, so one invocation cannot mix pipeline revisions.
|
downloaded remote session, so one invocation cannot mix pipeline revisions.
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
## Status And Purpose
|
## Status And Purpose
|
||||||
|
|
||||||
|
**Status: Completed**
|
||||||
|
|
||||||
This is the decision-complete implementation plan for Workstream 5, **Runner
|
This is the decision-complete implementation plan for Workstream 5, **Runner
|
||||||
lifecycle and pre-execution decisions**, in
|
lifecycle and pre-execution decisions**, in
|
||||||
[`refactoring.md`](refactoring.md). Workstreams 1 through 4 are complete.
|
[`refactoring.md`](refactoring.md). Workstreams 1 through 4 are complete.
|
||||||
Workstream 5 is the next pending workstream and is the only feature scope of
|
Workstream 5 is complete; Workstream 6 is the next pending workstream. This
|
||||||
this plan.
|
plan's implementation scope remained limited to Workstream 5.
|
||||||
|
|
||||||
The implementation must reduce the change blast and cognitive load of
|
The implementation must reduce the change blast and cognitive load of
|
||||||
`internal/app.executePlan` without changing the pipeline's observable behavior.
|
`internal/app.executePlan` without changing the pipeline's observable behavior.
|
||||||
@@ -408,7 +410,7 @@ are easier to read than the former monolith.
|
|||||||
|
|
||||||
## Stage 6 — Remove Global Runner Seams And Close Workstream 5
|
## Stage 6 — Remove Global Runner Seams And Close Workstream 5
|
||||||
|
|
||||||
**Status: Pending**
|
**Status: Completed**
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ manifest must remain synchronized with the progress ledger.
|
|||||||
|
|
||||||
### Workstream 5 — Runner lifecycle and pre-execution decisions
|
### Workstream 5 — Runner lifecycle and pre-execution decisions
|
||||||
|
|
||||||
**Status:** Pending
|
**Status:** Complete
|
||||||
|
|
||||||
**Audit sources:** H04, H07, H14, H15, H17, H34, and H35.
|
**Audit sources:** H04, H07, H14, H15, H17, H34, and H35.
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,7 @@ func TestExecuteRunStagePublishPropagatesSelectedArtifacts(t *testing.T) {
|
|||||||
|
|
||||||
var capturedStages []string
|
var capturedStages []string
|
||||||
var capturedArtifacts []string
|
var capturedArtifacts []string
|
||||||
origExecuteStagesFn := executeStagesFn
|
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
||||||
t.Cleanup(func() {
|
|
||||||
executeStagesFn = origExecuteStagesFn
|
|
||||||
})
|
|
||||||
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
|
||||||
for _, s := range plan.Stages() {
|
for _, s := range plan.Stages() {
|
||||||
capturedStages = append(capturedStages, s.Name())
|
capturedStages = append(capturedStages, s.Name())
|
||||||
}
|
}
|
||||||
@@ -52,7 +48,7 @@ func TestExecuteRunStagePublishPropagatesSelectedArtifacts(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute(
|
code := executeWithPipelineExecutor(
|
||||||
[]string{
|
[]string{
|
||||||
"run-stage", "publish", "2026-05-03",
|
"run-stage", "publish", "2026-05-03",
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
@@ -62,6 +58,7 @@ func TestExecuteRunStagePublishPropagatesSelectedArtifacts(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&stdout,
|
&stdout,
|
||||||
&stderr,
|
&stderr,
|
||||||
|
executor,
|
||||||
)
|
)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
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 capturedStages []string
|
||||||
var capturedForce bool
|
var capturedForce bool
|
||||||
origExecuteStagesFn := executeStagesFn
|
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
||||||
t.Cleanup(func() {
|
|
||||||
executeStagesFn = origExecuteStagesFn
|
|
||||||
})
|
|
||||||
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
|
||||||
for _, s := range plan.Stages() {
|
for _, s := range plan.Stages() {
|
||||||
capturedStages = append(capturedStages, s.Name())
|
capturedStages = append(capturedStages, s.Name())
|
||||||
}
|
}
|
||||||
@@ -182,10 +175,11 @@ func TestExecuteAnalyzeForceRunsAnalyze(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute(
|
code := executeWithPipelineExecutor(
|
||||||
[]string{"analyze", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath},
|
[]string{"analyze", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath},
|
||||||
&stdout,
|
&stdout,
|
||||||
&stderr,
|
&stderr,
|
||||||
|
executor,
|
||||||
)
|
)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
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)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
||||||
|
|
||||||
var capturedArtifacts []string
|
var capturedArtifacts []string
|
||||||
origExecuteStagesFn := executeStagesFn
|
executor := func(_ context.Context, _ *config.Config, _ BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
||||||
t.Cleanup(func() {
|
|
||||||
executeStagesFn = origExecuteStagesFn
|
|
||||||
})
|
|
||||||
executeStagesFn = func(_ context.Context, _ *config.Config, _ BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
|
||||||
capturedArtifacts = append([]string(nil), opts.SelectedArtifacts...)
|
capturedArtifacts = append([]string(nil), opts.SelectedArtifacts...)
|
||||||
return &RunSummary{ManifestPath: filepath.Join(workspaceRoot, "manifest.json"), Executed: []string{"analyze"}}, nil
|
return &RunSummary{ManifestPath: filepath.Join(workspaceRoot, "manifest.json"), Executed: []string{"analyze"}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute(
|
code := executeWithPipelineExecutor(
|
||||||
[]string{
|
[]string{
|
||||||
"analyze",
|
"analyze",
|
||||||
"2026-05-03",
|
"2026-05-03",
|
||||||
@@ -228,6 +218,7 @@ func TestExecuteAnalyzePropagatesSelectedArtifacts(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&stdout,
|
&stdout,
|
||||||
&stderr,
|
&stderr,
|
||||||
|
executor,
|
||||||
)
|
)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
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 capturedStages []string
|
||||||
var capturedForce bool
|
var capturedForce bool
|
||||||
var capturedArtifacts []string
|
var capturedArtifacts []string
|
||||||
origExecuteStagesFn := executeStagesFn
|
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
||||||
t.Cleanup(func() {
|
|
||||||
executeStagesFn = origExecuteStagesFn
|
|
||||||
})
|
|
||||||
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
|
||||||
for _, s := range plan.Stages() {
|
for _, s := range plan.Stages() {
|
||||||
capturedStages = append(capturedStages, s.Name())
|
capturedStages = append(capturedStages, s.Name())
|
||||||
}
|
}
|
||||||
@@ -317,10 +304,11 @@ func TestExecutePublishForceRunsPublish(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr 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"},
|
[]string{"publish", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--artifacts", "session_recap"},
|
||||||
&stdout,
|
&stdout,
|
||||||
&stderr,
|
&stderr,
|
||||||
|
executor,
|
||||||
)
|
)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
|
|||||||
@@ -48,9 +48,7 @@ func TestAssembledCanonicalAndAliasArtifactRegenerationRequestsMatch(t *testing.
|
|||||||
force bool
|
force bool
|
||||||
}
|
}
|
||||||
var captured []capturedRequest
|
var captured []capturedRequest
|
||||||
original := executeStagesFn
|
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
|
||||||
t.Cleanup(func() { executeStagesFn = original })
|
|
||||||
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
|
|
||||||
captured = append(captured, capturedRequest{
|
captured = append(captured, capturedRequest{
|
||||||
stages: plan.Names(), artifacts: append([]string(nil), options.SelectedArtifacts...), force: options.Force,
|
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",
|
"2026-05-03", "--force", "--from", "extract", "--through", "analyze",
|
||||||
"--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath,
|
"--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)
|
t.Fatalf("canonical unselected Run() error = %v", err)
|
||||||
}
|
}
|
||||||
selected := append(append([]string(nil), base...), "--artifacts", "session_recap")
|
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)
|
t.Fatalf("canonical selected Run() error = %v", err)
|
||||||
}
|
}
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
@@ -72,7 +70,7 @@ func TestAssembledCanonicalAndAliasArtifactRegenerationRequestsMatch(t *testing.
|
|||||||
"regenerate-artifacts", "2026-05-03", "--artifacts", "session_recap",
|
"regenerate-artifacts", "2026-05-03", "--artifacts", "session_recap",
|
||||||
"--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath,
|
"--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())
|
t.Fatalf("alias exit=%d stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
if len(captured) != 3 {
|
if len(captured) != 3 {
|
||||||
@@ -289,12 +287,10 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
|
|||||||
t.Fatalf("plan output = %q, want testing provenance", planned.String())
|
t.Fatalf("plan output = %q, want testing provenance", planned.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
original := executeStagesFn
|
|
||||||
t.Cleanup(func() { executeStagesFn = original })
|
|
||||||
var capturedCfg *config.Config
|
var capturedCfg *config.Config
|
||||||
var capturedPlan BoundedPlan
|
var capturedPlan BoundedPlan
|
||||||
var capturedOptions RunOptions
|
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
|
capturedCfg = cfg
|
||||||
capturedPlan = plan
|
capturedPlan = plan
|
||||||
capturedOptions = options
|
capturedOptions = options
|
||||||
@@ -302,7 +298,7 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
|
|||||||
}
|
}
|
||||||
runArgs := append(append([]string(nil), common...), "--from", "analyze", "--through", "analyze", "--artifacts", "character_items_arannis")
|
runArgs := append(append([]string(nil), common...), "--from", "analyze", "--through", "analyze", "--artifacts", "character_items_arannis")
|
||||||
var runOut bytes.Buffer
|
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)
|
t.Fatalf("bounded family run: %v", err)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(capturedPlan.Names(), []string{"analyze"}) {
|
if !reflect.DeepEqual(capturedPlan.Names(), []string{"analyze"}) {
|
||||||
|
|||||||
@@ -140,9 +140,7 @@ func TestRunPassesBoundedPlanToRunner(t *testing.T) {
|
|||||||
var capturedStages []string
|
var capturedStages []string
|
||||||
var capturedPlan BoundedPlan
|
var capturedPlan BoundedPlan
|
||||||
var capturedOptions RunOptions
|
var capturedOptions RunOptions
|
||||||
original := executeStagesFn
|
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
|
||||||
t.Cleanup(func() { executeStagesFn = original })
|
|
||||||
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
|
|
||||||
capturedStages = plan.Names()
|
capturedStages = plan.Names()
|
||||||
capturedPlan = plan
|
capturedPlan = plan
|
||||||
capturedOptions = options
|
capturedOptions = options
|
||||||
@@ -150,7 +148,7 @@ func TestRunPassesBoundedPlanToRunner(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := Run(context.Background(), []string{
|
err := runWithPipelineExecutor(context.Background(), []string{
|
||||||
"2026-05-03",
|
"2026-05-03",
|
||||||
"--from", "render",
|
"--from", "render",
|
||||||
"--through", "extract",
|
"--through", "extract",
|
||||||
@@ -158,7 +156,7 @@ func TestRunPassesBoundedPlanToRunner(t *testing.T) {
|
|||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
"--campaign-file", campaignPath,
|
"--campaign-file", campaignPath,
|
||||||
"--session", sessionPath,
|
"--session", sessionPath,
|
||||||
}, &out)
|
}, &out, executor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Run() error = %v", err)
|
t.Fatalf("Run() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import (
|
|||||||
|
|
||||||
var supportedCommands = []string{"version", "run", "regenerate-artifacts", "run-stage", "analyze", "publish", "clean", "session", "config"}
|
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.
|
// Execute dispatches CLI commands and returns a process exit code.
|
||||||
func Execute(args []string, stdout, stderr io.Writer) int {
|
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 {
|
if len(args) == 0 {
|
||||||
printUsage(stderr)
|
printUsage(stderr)
|
||||||
return 1
|
return 1
|
||||||
@@ -27,15 +29,15 @@ func Execute(args []string, stdout, stderr io.Writer) int {
|
|||||||
case "version":
|
case "version":
|
||||||
err = Version(cmdArgs, stdout)
|
err = Version(cmdArgs, stdout)
|
||||||
case "run":
|
case "run":
|
||||||
err = runCommandFn(ctx, cmdArgs, stdout)
|
err = runWithPipelineExecutor(ctx, cmdArgs, stdout, executor)
|
||||||
case "regenerate-artifacts":
|
case "regenerate-artifacts":
|
||||||
err = RegenerateArtifacts(ctx, cmdArgs, stdout)
|
err = regenerateArtifactsWithPipelineExecutor(ctx, cmdArgs, stdout, executor)
|
||||||
case "run-stage":
|
case "run-stage":
|
||||||
err = RunStage(ctx, cmdArgs, stdout)
|
err = runStageWithPipelineExecutor(ctx, cmdArgs, stdout, executor)
|
||||||
case "analyze":
|
case "analyze":
|
||||||
err = Analyze(ctx, cmdArgs, stdout)
|
err = analyzeWithPipelineExecutor(ctx, cmdArgs, stdout, executor)
|
||||||
case "publish":
|
case "publish":
|
||||||
err = Publish(ctx, cmdArgs, stdout)
|
err = publishWithPipelineExecutor(ctx, cmdArgs, stdout, executor)
|
||||||
case "session":
|
case "session":
|
||||||
err = Session(ctx, cmdArgs, stdout)
|
err = Session(ctx, cmdArgs, stdout)
|
||||||
case "clean":
|
case "clean":
|
||||||
|
|||||||
@@ -16,26 +16,24 @@ func TestRunProfileSelectionFlowsThroughSharedLoader(t *testing.T) {
|
|||||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
enableCommandTestProfiles(t, pipelinePath)
|
enableCommandTestProfiles(t, pipelinePath)
|
||||||
|
|
||||||
original := executeStagesFn
|
|
||||||
t.Cleanup(func() { executeStagesFn = original })
|
|
||||||
var language string
|
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
|
language = cfg.Pipeline.WhisperX.Language
|
||||||
return &RunSummary{SessionID: cfg.Session.SessionID, ManifestPath: "manifest.json"}, nil
|
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",
|
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--profile", "testing",
|
||||||
}, io.Discard); err != nil {
|
}, io.Discard, executor); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if language != "fr" {
|
if language != "fr" {
|
||||||
t.Fatalf("explicit profile language = %q, want fr", language)
|
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,
|
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath,
|
||||||
}, io.Discard); err != nil {
|
}, io.Discard, executor); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if language != "en" {
|
if language != "en" {
|
||||||
@@ -43,9 +41,9 @@ func TestRunProfileSelectionFlowsThroughSharedLoader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, profile := range []string{"unknown", ""} {
|
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,
|
"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") {
|
if err == nil || !strings.Contains(err.Error(), "profile") {
|
||||||
t.Fatalf("profile %q error = %v, want selection rejection", profile, err)
|
t.Fatalf("profile %q error = %v, want selection rejection", profile, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import (
|
|||||||
// RegenerateArtifacts expands the convenience command into its canonical run
|
// RegenerateArtifacts expands the convenience command into its canonical run
|
||||||
// invocation. The run command remains the sole owner of parsing and execution.
|
// invocation. The run command remains the sole owner of parsing and execution.
|
||||||
func RegenerateArtifacts(ctx context.Context, args []string, out io.Writer) error {
|
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) {
|
if containsHelpOption(args) {
|
||||||
printRegenerateArtifactsHelp(out)
|
printRegenerateArtifactsHelp(out)
|
||||||
return nil
|
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, "--force", "--from", "extract", "--through", "analyze")
|
||||||
expanded = append(expanded, args...)
|
expanded = append(expanded, args...)
|
||||||
return runCommandFn(ctx, expanded, out)
|
return runWithPipelineExecutor(ctx, expanded, out, executor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsHelpOption(args []string) bool {
|
func containsHelpOption(args []string) bool {
|
||||||
|
|||||||
@@ -7,82 +7,80 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRegenerateArtifactsForwardsExactCanonicalRunArguments(t *testing.T) {
|
func TestRegenerateArtifactsForwardsCanonicalPipelineRequest(t *testing.T) {
|
||||||
original := runCommandFn
|
workspaceRoot := t.TempDir()
|
||||||
t.Cleanup(func() { runCommandFn = original })
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
||||||
var captured []string
|
enableCommandTestProfiles(t, pipelinePath)
|
||||||
runCommandFn = func(_ context.Context, args []string, _ io.Writer) error {
|
var capturedPlan BoundedPlan
|
||||||
captured = append([]string(nil), args...)
|
var capturedOptions RunOptions
|
||||||
return nil
|
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",
|
"regenerate-artifacts", "2026-05-03",
|
||||||
"--artifacts", "session_recap,player_handout",
|
"--artifacts", "session_recap,player_handout",
|
||||||
"--artifacts=player_handout",
|
"--artifacts=player_handout",
|
||||||
"--config", "pipeline.yml",
|
"--config", pipelinePath,
|
||||||
"--campaign", "sample-campaign",
|
"--campaign-file", campaignPath,
|
||||||
|
"--session", sessionPath,
|
||||||
"--profile", "testing",
|
"--profile", "testing",
|
||||||
}, io.Discard, io.Discard)
|
}, io.Discard, io.Discard, executor)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("Execute() code = %d, want 0", code)
|
t.Fatalf("executeWithPipelineExecutor() code = %d, want 0", code)
|
||||||
}
|
}
|
||||||
want := []string{
|
if got, want := capturedPlan.Names(), []string{"extract", "analyze"}; !reflect.DeepEqual(got, want) {
|
||||||
"2026-05-03", "--force", "--from", "extract", "--through", "analyze",
|
t.Fatalf("plan = %#v, want %#v", got, want)
|
||||||
"--artifacts", "session_recap,player_handout",
|
|
||||||
"--artifacts=player_handout",
|
|
||||||
"--config", "pipeline.yml",
|
|
||||||
"--campaign", "sample-campaign",
|
|
||||||
"--profile", "testing",
|
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(captured, want) {
|
if !capturedOptions.Force {
|
||||||
t.Fatalf("forwarded args = %#v, want %#v", captured, want)
|
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) {
|
func TestRegenerateArtifactsForwardsSessionIDCompatibilityFlag(t *testing.T) {
|
||||||
original := runCommandFn
|
workspaceRoot := t.TempDir()
|
||||||
t.Cleanup(func() { runCommandFn = original })
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
||||||
var captured []string
|
var capturedSessionID string
|
||||||
runCommandFn = func(_ context.Context, args []string, _ io.Writer) error {
|
executor := func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
|
||||||
captured = append([]string(nil), args...)
|
capturedSessionID = cfg.Session.SessionID
|
||||||
return nil
|
return &RunSummary{SessionID: cfg.Session.SessionID, ManifestPath: "manifest.json"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
code := Execute([]string{
|
code := executeWithPipelineExecutor([]string{
|
||||||
"regenerate-artifacts",
|
"regenerate-artifacts",
|
||||||
"--config", "pipeline.yml",
|
"--config", pipelinePath,
|
||||||
|
"--campaign-file", campaignPath,
|
||||||
|
"--session", sessionPath,
|
||||||
"--session-id", "2026-05-03",
|
"--session-id", "2026-05-03",
|
||||||
"--artifacts", "session_recap",
|
"--artifacts", "session_recap",
|
||||||
}, io.Discard, io.Discard)
|
}, io.Discard, io.Discard, executor)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("Execute() code = %d, want 0", code)
|
t.Fatalf("executeWithPipelineExecutor() code = %d, want 0", code)
|
||||||
}
|
}
|
||||||
want := []string{
|
if capturedSessionID != "2026-05-03" {
|
||||||
"--force", "--from", "extract", "--through", "analyze",
|
t.Fatalf("session id = %q, want compatibility flag value", capturedSessionID)
|
||||||
"--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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegenerateArtifactsHelpDoesNotInvokeRun(t *testing.T) {
|
func TestRegenerateArtifactsHelpDoesNotInvokeRun(t *testing.T) {
|
||||||
original := runCommandFn
|
|
||||||
t.Cleanup(func() { runCommandFn = original })
|
|
||||||
called := false
|
called := false
|
||||||
runCommandFn = func(_ context.Context, _ []string, _ io.Writer) error {
|
executor := func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error) {
|
||||||
called = true
|
called = true
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr 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())
|
t.Fatalf("Execute() code = %d, stderr = %q", code, stderr.String())
|
||||||
}
|
}
|
||||||
if called {
|
if called {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func TestRemoteSessionConfigIsRemovedAfterEveryCommandExit(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
sessionYAML string
|
sessionYAML string
|
||||||
command []string
|
command []string
|
||||||
configureRun func()
|
executor pipelineExecutor
|
||||||
wantSuccessful bool
|
wantSuccessful bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -126,10 +126,8 @@ inputs:
|
|||||||
prefix: audio/
|
prefix: audio/
|
||||||
`,
|
`,
|
||||||
command: []string{"run", "2026-05-03"},
|
command: []string{"run", "2026-05-03"},
|
||||||
configureRun: func() {
|
executor: func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error) {
|
||||||
executeStagesFn = func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error) {
|
return nil, errors.New("adapter failed")
|
||||||
return nil, errors.New("adapter failed")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -140,10 +138,8 @@ inputs:
|
|||||||
prefix: audio/
|
prefix: audio/
|
||||||
`,
|
`,
|
||||||
command: []string{"run", "2026-05-03"},
|
command: []string{"run", "2026-05-03"},
|
||||||
configureRun: func() {
|
executor: func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error) {
|
||||||
executeStagesFn = func(context.Context, *config.Config, BoundedPlan, RunOptions) (*RunSummary, error) {
|
return nil, context.Canceled
|
||||||
return nil, context.Canceled
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -159,16 +155,15 @@ inputs:
|
|||||||
|
|
||||||
var downloadedPath string
|
var downloadedPath string
|
||||||
captureRemoteSessionTempPath(t, &downloadedPath)
|
captureRemoteSessionTempPath(t, &downloadedPath)
|
||||||
if tt.configureRun != nil {
|
executor := tt.executor
|
||||||
origExecuteStagesFn := executeStagesFn
|
if executor == nil {
|
||||||
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
|
executor = executePlan
|
||||||
tt.configureRun()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
args := append(append([]string(nil), tt.command...), "--config", pipelinePath, "--campaign-file", campaignPath)
|
args := append(append([]string(nil), tt.command...), "--config", pipelinePath, "--campaign-file", campaignPath)
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute(args, &stdout, &stderr)
|
code := executeWithPipelineExecutor(args, &stdout, &stderr, executor)
|
||||||
if tt.wantSuccessful && code != 0 {
|
if tt.wantSuccessful && code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
|
|||||||
|
|
||||||
restoreWithStoreAndRealPhases(t, fake)
|
restoreWithStoreAndRealPhases(t, fake)
|
||||||
|
|
||||||
origExecuteStagesFn := executeStagesFn
|
executor := func(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
||||||
t.Cleanup(func() {
|
|
||||||
executeStagesFn = origExecuteStagesFn
|
|
||||||
})
|
|
||||||
executeStagesFn = func(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
|
||||||
if opts.Env == nil {
|
if opts.Env == nil {
|
||||||
opts.Env = &Env{}
|
opts.Env = &Env{}
|
||||||
}
|
}
|
||||||
@@ -81,7 +77,7 @@ func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
|
|||||||
|
|
||||||
stdout.Reset()
|
stdout.Reset()
|
||||||
stderr.Reset()
|
stderr.Reset()
|
||||||
runStageCode := Execute(
|
runStageCode := executeWithPipelineExecutor(
|
||||||
[]string{
|
[]string{
|
||||||
"run-stage", "analyze", cfg.Session.SessionID,
|
"run-stage", "analyze", cfg.Session.SessionID,
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
@@ -92,6 +88,7 @@ func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&stdout,
|
&stdout,
|
||||||
&stderr,
|
&stderr,
|
||||||
|
executor,
|
||||||
)
|
)
|
||||||
if runStageCode != 0 {
|
if runStageCode != 0 {
|
||||||
t.Fatalf("run-stage exit code = %d, want 0; stderr=%q", runStageCode, stderr.String())
|
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")
|
mustReadEquals(t, filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md"), "# previous recap\n")
|
||||||
|
|
||||||
scriptoriumFake := &scriptorium.FakeRunner{}
|
scriptoriumFake := &scriptorium.FakeRunner{}
|
||||||
origExecuteStagesFn := executeStagesFn
|
|
||||||
origObjectStoreFn := newObjectStoreFromConfigFn
|
origObjectStoreFn := newObjectStoreFromConfigFn
|
||||||
objectStoreConstructed := false
|
objectStoreConstructed := false
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
executeStagesFn = origExecuteStagesFn
|
|
||||||
newObjectStoreFromConfigFn = origObjectStoreFn
|
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 {
|
if opts.Env == nil {
|
||||||
opts.Env = &Env{}
|
opts.Env = &Env{}
|
||||||
}
|
}
|
||||||
@@ -230,7 +225,7 @@ previous_session_id: 2026-04-26
|
|||||||
|
|
||||||
stdout.Reset()
|
stdout.Reset()
|
||||||
stderr.Reset()
|
stderr.Reset()
|
||||||
runStageCode := Execute(
|
runStageCode := executeWithPipelineExecutor(
|
||||||
[]string{
|
[]string{
|
||||||
"run-stage", "analyze", cfg.Session.SessionID,
|
"run-stage", "analyze", cfg.Session.SessionID,
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
@@ -241,6 +236,7 @@ previous_session_id: 2026-04-26
|
|||||||
},
|
},
|
||||||
&stdout,
|
&stdout,
|
||||||
&stderr,
|
&stderr,
|
||||||
|
executor,
|
||||||
)
|
)
|
||||||
if runStageCode != 0 {
|
if runStageCode != 0 {
|
||||||
t.Fatalf("run-stage exit code = %d, want 0; stderr=%q", runStageCode, stderr.String())
|
t.Fatalf("run-stage exit code = %d, want 0; stderr=%q", runStageCode, stderr.String())
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import (
|
|||||||
|
|
||||||
// Run executes the pipeline plan and persists manifest state.
|
// Run executes the pipeline plan and persists manifest state.
|
||||||
func Run(ctx context.Context, args []string, out io.Writer) error {
|
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)
|
request, err := parseBoundedRunRequest("run", args, out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, flag.ErrHelp) {
|
if errors.Is(err, flag.ErrHelp) {
|
||||||
@@ -37,7 +41,7 @@ func Run(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run: %w", err)
|
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,
|
Force: request.Force,
|
||||||
SelectedArtifacts: selectedArtifacts,
|
SelectedArtifacts: selectedArtifacts,
|
||||||
EffectiveArtifacts: effectiveArtifacts,
|
EffectiveArtifacts: effectiveArtifacts,
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import (
|
|||||||
|
|
||||||
// RunStage executes exactly one selected stage.
|
// RunStage executes exactly one selected stage.
|
||||||
func RunStage(ctx context.Context, args []string, out io.Writer) error {
|
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 stageName string
|
||||||
var positionalSessionID string
|
var positionalSessionID string
|
||||||
if len(args) >= 2 && !isCLIFlagToken(args[0]) && !isCLIFlagToken(args[1]) {
|
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,
|
PreviousSessionID: flags.previousSessionID,
|
||||||
Force: force,
|
Force: force,
|
||||||
SelectedArtifacts: normalizedArtifacts,
|
SelectedArtifacts: normalizedArtifacts,
|
||||||
})
|
}, executor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -93,6 +97,10 @@ func RunStage(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
|
|
||||||
// Analyze force-runs the analyze stage.
|
// Analyze force-runs the analyze stage.
|
||||||
func Analyze(ctx context.Context, args []string, out io.Writer) error {
|
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 := flag.NewFlagSet("analyze", flag.ContinueOnError)
|
||||||
fs.SetOutput(io.Discard)
|
fs.SetOutput(io.Discard)
|
||||||
|
|
||||||
@@ -123,7 +131,7 @@ func Analyze(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
PreviousSessionID: flags.previousSessionID,
|
PreviousSessionID: flags.previousSessionID,
|
||||||
Force: true,
|
Force: true,
|
||||||
SelectedArtifacts: normalizedArtifacts,
|
SelectedArtifacts: normalizedArtifacts,
|
||||||
})
|
}, executor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -140,6 +148,10 @@ func Analyze(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
|
|
||||||
// Publish force-runs the publish stage.
|
// Publish force-runs the publish stage.
|
||||||
func Publish(ctx context.Context, args []string, out io.Writer) error {
|
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 := flag.NewFlagSet("publish", flag.ContinueOnError)
|
||||||
fs.SetOutput(io.Discard)
|
fs.SetOutput(io.Discard)
|
||||||
|
|
||||||
@@ -170,7 +182,7 @@ func Publish(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
PreviousSessionID: flags.previousSessionID,
|
PreviousSessionID: flags.previousSessionID,
|
||||||
Force: true,
|
Force: true,
|
||||||
SelectedArtifacts: normalizedArtifacts,
|
SelectedArtifacts: normalizedArtifacts,
|
||||||
})
|
}, executor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -198,7 +210,7 @@ type singleStageCommand struct {
|
|||||||
SelectedArtifacts []string
|
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)
|
plan, err := buildSingleStageExecutionPlan(req.StageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", req.CommandName, err)
|
return nil, fmt.Errorf("%s: %w", req.CommandName, err)
|
||||||
@@ -224,7 +236,7 @@ func runSingleStageCommand(ctx context.Context, req singleStageCommand) (*RunSum
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", req.CommandName, err)
|
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,
|
Force: req.Force,
|
||||||
SelectedArtifacts: selectedArtifacts,
|
SelectedArtifacts: selectedArtifacts,
|
||||||
EffectiveArtifacts: effectiveArtifacts,
|
EffectiveArtifacts: effectiveArtifacts,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ type RunSummary struct {
|
|||||||
Skipped []string
|
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) {
|
func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions) (summary *RunSummary, resultErr error) {
|
||||||
return executePlanWithDependencies(ctx, cfg, plan, opts, invocationDependencies{})
|
return executePlanWithDependencies(ctx, cfg, plan, opts, invocationDependencies{})
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ func TestExecuteRunAcceptsPositionalSessionID(t *testing.T) {
|
|||||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
var capturedSessionID string
|
var capturedSessionID string
|
||||||
origExecuteStagesFn := executeStagesFn
|
executor := func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
|
||||||
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
|
|
||||||
executeStagesFn = func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
|
|
||||||
capturedSessionID = cfg.Session.SessionID
|
capturedSessionID = cfg.Session.SessionID
|
||||||
return &RunSummary{
|
return &RunSummary{
|
||||||
SessionID: cfg.Session.SessionID,
|
SessionID: cfg.Session.SessionID,
|
||||||
@@ -31,13 +29,13 @@ func TestExecuteRunAcceptsPositionalSessionID(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{
|
code := executeWithPipelineExecutor([]string{
|
||||||
"run",
|
"run",
|
||||||
"2026-05-03",
|
"2026-05-03",
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
"--campaign-file", campaignPath,
|
"--campaign-file", campaignPath,
|
||||||
"--session", sessionPath,
|
"--session", sessionPath,
|
||||||
}, &stdout, &stderr)
|
}, &stdout, &stderr, executor)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
@@ -114,9 +112,7 @@ inputs:
|
|||||||
audio_s3:
|
audio_s3:
|
||||||
prefix: audio/
|
prefix: audio/
|
||||||
`)
|
`)
|
||||||
origExecuteStagesFn := executeStagesFn
|
executor := func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
|
||||||
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
|
|
||||||
executeStagesFn = func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
|
|
||||||
return &RunSummary{
|
return &RunSummary{
|
||||||
SessionID: cfg.Session.SessionID,
|
SessionID: cfg.Session.SessionID,
|
||||||
ManifestPath: filepath.Join(workspaceRoot, "manifest.json"),
|
ManifestPath: filepath.Join(workspaceRoot, "manifest.json"),
|
||||||
@@ -126,12 +122,12 @@ inputs:
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{
|
code := executeWithPipelineExecutor([]string{
|
||||||
"run",
|
"run",
|
||||||
"2026-06-07",
|
"2026-06-07",
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
"--campaign-file", campaignPath,
|
"--campaign-file", campaignPath,
|
||||||
}, &stdout, &stderr)
|
}, &stdout, &stderr, executor)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
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 capturedStages []string
|
||||||
var capturedForce bool
|
var capturedForce bool
|
||||||
var capturedArtifacts []string
|
var capturedArtifacts []string
|
||||||
origExecuteStagesFn := executeStagesFn
|
executor := func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
||||||
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
|
|
||||||
executeStagesFn = func(_ context.Context, _ *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) {
|
|
||||||
for _, s := range plan.Stages() {
|
for _, s := range plan.Stages() {
|
||||||
capturedStages = append(capturedStages, s.Name())
|
capturedStages = append(capturedStages, s.Name())
|
||||||
}
|
}
|
||||||
@@ -208,7 +202,7 @@ func TestExecuteWorkflowCommandsAcceptPositionalSessionID(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute(tt.args, &stdout, &stderr)
|
code := executeWithPipelineExecutor(tt.args, &stdout, &stderr, executor)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user