198 lines
7.2 KiB
Go
198 lines
7.2 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
)
|
|
|
|
func TestBoundedRunParsingIsSharedByRunAndPlan(t *testing.T) {
|
|
args := []string{
|
|
"2026-05-03",
|
|
"--from", "extract",
|
|
"--through=publish",
|
|
"--force",
|
|
"--artifacts", "session_recap,player_handout",
|
|
"--artifacts=session_recap",
|
|
"--config", "pipeline.yml",
|
|
}
|
|
runRequest, err := parseBoundedRunRequest("run", args, io.Discard)
|
|
if err != nil {
|
|
t.Fatalf("parse run request: %v", err)
|
|
}
|
|
planRequest, err := parseBoundedRunRequest("plan", args, io.Discard)
|
|
if err != nil {
|
|
t.Fatalf("parse plan request: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(runRequest.Plan.Names(), planRequest.Plan.Names()) ||
|
|
runRequest.Plan.From() != planRequest.Plan.From() ||
|
|
runRequest.Plan.Through() != planRequest.Plan.Through() ||
|
|
runRequest.Force != planRequest.Force ||
|
|
!reflect.DeepEqual(runRequest.SelectedArtifacts, planRequest.SelectedArtifacts) ||
|
|
runRequest.Config != planRequest.Config {
|
|
t.Fatalf("run request = %#v, plan request = %#v", runRequest, planRequest)
|
|
}
|
|
wantArtifacts := []string{"player_handout", "session_recap"}
|
|
if !reflect.DeepEqual(runRequest.SelectedArtifacts, wantArtifacts) {
|
|
t.Fatalf("artifacts = %#v, want %#v", runRequest.SelectedArtifacts, wantArtifacts)
|
|
}
|
|
}
|
|
|
|
func TestBoundedRunParsingRejectsDuplicateSingletons(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{
|
|
{name: "from separate", args: []string{"session", "--from", "render", "--from", "extract"}, want: "--from may be specified only once"},
|
|
{name: "from equals", args: []string{"session", "--from=render", "--from=extract"}, want: "--from may be specified only once"},
|
|
{name: "through mixed", args: []string{"session", "--through", "analyze", "--through=publish"}, want: "--through may be specified only once"},
|
|
{name: "force separate", args: []string{"session", "--force", "--force"}, want: "--force may be specified only once"},
|
|
{name: "force equals", args: []string{"session", "--force=true", "--force=false"}, want: "--force may be specified only once"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := parseBoundedRunRequest("run", test.args, io.Discard)
|
|
if err == nil || !strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("error = %v, want %q", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBoundedRunParsingUsesSharedRangeValidation(t *testing.T) {
|
|
args := []string{"session", "--from", "publish", "--through", "render"}
|
|
runRequest, runErr := parseBoundedRunRequest("run", args, io.Discard)
|
|
planRequest, planErr := parseBoundedRunRequest("plan", args, io.Discard)
|
|
if runErr == nil || planErr == nil {
|
|
t.Fatalf("run request=%#v error=%v; plan request=%#v error=%v", runRequest, runErr, planRequest, planErr)
|
|
}
|
|
runDetail := strings.TrimPrefix(runErr.Error(), "run: ")
|
|
planDetail := strings.TrimPrefix(planErr.Error(), "plan: ")
|
|
if runDetail != planDetail || !strings.Contains(runDetail, `from stage "publish" occurs after through stage "render"`) {
|
|
t.Fatalf("run error = %q, plan error = %q", runErr, planErr)
|
|
}
|
|
}
|
|
|
|
func TestBoundedRunParsingGatesArtifactSelectionByRange(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
through string
|
|
wantErr bool
|
|
}{
|
|
{name: "render only", through: "render", wantErr: true},
|
|
{name: "analyze only", through: "analyze"},
|
|
{name: "publish only", through: "publish"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := parseBoundedRunRequest("run", []string{
|
|
"session", "--from", test.through, "--through", test.through, "--artifacts", "session_recap",
|
|
}, io.Discard)
|
|
if test.wantErr && (err == nil || !strings.Contains(err.Error(), "range containing analyze or publish")) {
|
|
t.Fatalf("error = %v, want artifact/range error", err)
|
|
}
|
|
if !test.wantErr && err != nil {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunPassesBoundedPlanToRunner(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
|
|
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) {
|
|
capturedStages = plan.Names()
|
|
capturedPlan = plan
|
|
capturedOptions = options
|
|
return &RunSummary{SessionID: "2026-05-03", ManifestPath: filepath.Join(workspaceRoot, "manifest.json")}, nil
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
err := Run(context.Background(), []string{
|
|
"2026-05-03",
|
|
"--from", "render",
|
|
"--through", "extract",
|
|
"--force",
|
|
"--config", pipelinePath,
|
|
"--campaign-file", campaignPath,
|
|
"--session", sessionPath,
|
|
}, &out)
|
|
if err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
want := []string{"render", "extract"}
|
|
if !reflect.DeepEqual(capturedStages, want) || !reflect.DeepEqual(capturedPlan.Names(), want) || !capturedOptions.Force {
|
|
t.Fatalf("stages = %#v options = %#v", capturedStages, capturedOptions)
|
|
}
|
|
}
|
|
|
|
func TestPlanPrintsOnlyBoundedRange(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC))
|
|
m.Campaign = "sample-campaign"
|
|
for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim"} {
|
|
m.MarkStageSucceeded(name, time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil)
|
|
}
|
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
|
if err := (&manifest.LocalStore{}).Save(context.Background(), manifestPath, m); err != nil {
|
|
t.Fatalf("save prerequisite manifest: %v", err)
|
|
}
|
|
var out bytes.Buffer
|
|
err := Plan(context.Background(), []string{
|
|
"2026-05-03",
|
|
"--from", "render",
|
|
"--through", "extract",
|
|
"--config", pipelinePath,
|
|
"--campaign-file", campaignPath,
|
|
"--session", sessionPath,
|
|
}, &out)
|
|
if err != nil {
|
|
t.Fatalf("Plan() error = %v", err)
|
|
}
|
|
got := out.String()
|
|
if !strings.Contains(got, "render: run\nextract: run\ntotals: run=2 skip=0") {
|
|
t.Fatalf("output = %q, want bounded decisions", got)
|
|
}
|
|
if strings.Contains(got, "trim: ") || strings.Contains(got, "analyze: ") {
|
|
t.Fatalf("output = %q, contains excluded stages", got)
|
|
}
|
|
}
|
|
|
|
func TestBoundedRunCommandHelp(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{
|
|
{name: "run", args: []string{"run", "--help"}, want: "Usage: narratio run <session_id> [--from <stage>] [--through <stage>]"},
|
|
{name: "plan", args: []string{"session", "plan", "--help"}, want: "Usage: narratio session plan <session_id> [--from <stage>] [--through <stage>]"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
if code := Execute(test.args, &stdout, &stderr); code != 0 {
|
|
t.Fatalf("exit code = %d, stderr = %q", code, stderr.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), test.want) || stderr.Len() != 0 {
|
|
t.Fatalf("stdout = %q stderr = %q, want %q", stdout.String(), stderr.String(), test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|