Carry dependencies through operator commands
This commit is contained in:
@@ -199,7 +199,7 @@ select the real storage constructor, and focused tests remain offline.
|
||||
|
||||
## Stage 3 — Carry Dependencies Through Operator Commands
|
||||
|
||||
**Status:** Pending
|
||||
**Status:** Completed
|
||||
|
||||
### Goal
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ import (
|
||||
// Clean removes local workspace/spool state while preserving durable cache
|
||||
// state unless cache cleanup is explicitly requested.
|
||||
func Clean(ctx context.Context, args []string, out io.Writer) error {
|
||||
return cleanWithDependencies(ctx, args, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func cleanWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error {
|
||||
deps = normalizeCommandDependencies(deps)
|
||||
fs := flag.NewFlagSet("clean", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
@@ -31,16 +36,20 @@ func Clean(ctx context.Context, args []string, out io.Writer) error {
|
||||
return err
|
||||
}
|
||||
if all {
|
||||
return cleanAllLocal(flags, dryRun, clearCache, out)
|
||||
return cleanAllLocalWithDependencies(flags, dryRun, clearCache, out, deps)
|
||||
}
|
||||
return cleanSession(ctx, flags, dryRun, clearCache, out)
|
||||
return cleanSessionWithDependencies(ctx, flags, dryRun, clearCache, out, deps)
|
||||
}
|
||||
|
||||
func cleanSession(ctx context.Context, flags commonConfigFlags, dryRun, clearCache bool, out io.Writer) error {
|
||||
return cleanSessionWithDependencies(ctx, flags, dryRun, clearCache, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func cleanSessionWithDependencies(ctx context.Context, flags commonConfigFlags, dryRun, clearCache bool, out io.Writer, deps commandDependencies) error {
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("clean: session_id is required unless --all is set")
|
||||
}
|
||||
loaded, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions())
|
||||
loaded, err := loadCommandConfigWithDependencies(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions(), deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("clean: %w", err)
|
||||
}
|
||||
@@ -71,7 +80,7 @@ func cleanSession(ctx context.Context, flags commonConfigFlags, dryRun, clearCac
|
||||
}
|
||||
|
||||
if clearCache {
|
||||
if err := cleanSessionAudioCache(ctx, cfg, dryRun, out); err != nil {
|
||||
if err := cleanSessionAudioCacheWithDependencies(ctx, cfg, dryRun, out, deps); err != nil {
|
||||
return fmt.Errorf("clean: %w", err)
|
||||
}
|
||||
} else {
|
||||
@@ -81,6 +90,10 @@ func cleanSession(ctx context.Context, flags commonConfigFlags, dryRun, clearCac
|
||||
}
|
||||
|
||||
func cleanAllLocal(flags commonConfigFlags, dryRun, clearCache bool, out io.Writer) error {
|
||||
return cleanAllLocalWithDependencies(flags, dryRun, clearCache, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func cleanAllLocalWithDependencies(flags commonConfigFlags, dryRun, clearCache bool, out io.Writer, deps commandDependencies) error {
|
||||
if strings.TrimSpace(flags.campaignPath) != "" ||
|
||||
strings.TrimSpace(flags.campaignFilePath) != "" ||
|
||||
strings.TrimSpace(flags.sessionPath) != "" ||
|
||||
@@ -88,7 +101,7 @@ func cleanAllLocal(flags commonConfigFlags, dryRun, clearCache bool, out io.Writ
|
||||
strings.TrimSpace(flags.previousSessionID) != "" {
|
||||
return fmt.Errorf("clean: --all cannot be combined with --campaign, --campaign-file, --session, a session_id, or --previous-session-id")
|
||||
}
|
||||
_, pipelineCfg, err := loadPipelineConfig(flags.pipelinePath, config.PipelineLoadOptions{Profile: flags.profile.pointer()})
|
||||
_, pipelineCfg, err := loadPipelineConfigWithDependencies(flags.pipelinePath, config.PipelineLoadOptions{Profile: flags.profile.pointer()}, deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("clean: %w", err)
|
||||
}
|
||||
@@ -195,6 +208,10 @@ func cleanableRootChildren(root, policy string) (string, []string, error) {
|
||||
}
|
||||
|
||||
func cleanSessionAudioCache(ctx context.Context, cfg *config.Config, dryRun bool, out io.Writer) error {
|
||||
return cleanSessionAudioCacheWithDependencies(ctx, cfg, dryRun, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func cleanSessionAudioCacheWithDependencies(ctx context.Context, cfg *config.Config, dryRun bool, out io.Writer, deps commandDependencies) error {
|
||||
if cfg.Session.Inputs.AudioS3 == nil {
|
||||
fmt.Fprintln(out, "Cache: skipped (session does not use audio_s3)")
|
||||
return nil
|
||||
@@ -202,7 +219,7 @@ func cleanSessionAudioCache(ctx context.Context, cfg *config.Config, dryRun bool
|
||||
if cfg.Pipeline.Storage.S3 == nil || strings.TrimSpace(cfg.Pipeline.Storage.S3.Bucket) == "" {
|
||||
return fmt.Errorf("clear cache requires pipeline.storage.s3.bucket")
|
||||
}
|
||||
store, err := newCommandObjectStore(ctx, cfg, nil)
|
||||
store, err := newCommandObjectStoreWithDependencies(ctx, cfg, nil, deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize object store for cache cleanup: %w", err)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ inputs:
|
||||
fake := &storage.FakeBackend{}
|
||||
fake.SeedObject(storage.FakeObject{Key: audioKey, Data: []byte("audio")})
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
cacheRoot := filepath.Join(workspaceRoot, "cache")
|
||||
cachePath, err := artifacts.S3AudioCachePath(cacheRoot, "test-bucket", audioKey)
|
||||
@@ -105,7 +105,7 @@ inputs:
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{"clean", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--clear-cache"}, &stdout, &stderr)
|
||||
code := executeWithDependencies([]string{"clean", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--clear-cache"}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func executeWithDependencies(args []string, stdout, stderr io.Writer, deps comma
|
||||
case "session":
|
||||
err = sessionWithDependencies(ctx, cmdArgs, stdout, deps)
|
||||
case "clean":
|
||||
err = Clean(ctx, cmdArgs, stdout)
|
||||
err = cleanWithDependencies(ctx, cmdArgs, stdout, deps)
|
||||
case "config":
|
||||
err = Config(ctx, cmdArgs, stdout)
|
||||
default:
|
||||
|
||||
@@ -35,9 +35,7 @@ func retainedCommandConfig(cfg *config.Config) *commandConfig {
|
||||
}
|
||||
|
||||
func loadCommandConfig(ctx context.Context, pipelineFlag, campaignFlag, campaignFileFlag, sessionFlag string, sessionOpts config.SessionLoadOptions) (loaded *commandConfig, err error) {
|
||||
return loadCommandConfigWithDependencies(ctx, pipelineFlag, campaignFlag, campaignFileFlag, sessionFlag, sessionOpts, commandDependencies{
|
||||
newObjectStore: newObjectStoreFromConfigFn,
|
||||
})
|
||||
return loadCommandConfigWithDependencies(ctx, pipelineFlag, campaignFlag, campaignFileFlag, sessionFlag, sessionOpts, commandDependencies{})
|
||||
}
|
||||
|
||||
func loadCommandConfigWithDependencies(ctx context.Context, pipelineFlag, campaignFlag, campaignFileFlag, sessionFlag string, sessionOpts config.SessionLoadOptions, deps commandDependencies) (loaded *commandConfig, err error) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func newCommandObjectStore(ctx context.Context, cfg *config.Config, logger *slog.Logger) (storage.ObjectStore, error) {
|
||||
return newCommandObjectStoreWithDependencies(ctx, cfg, logger, commandDependencies{newObjectStore: newObjectStoreFromConfigFn})
|
||||
return newCommandObjectStoreWithDependencies(ctx, cfg, logger, commandDependencies{})
|
||||
}
|
||||
|
||||
func newCommandObjectStoreWithDependencies(ctx context.Context, cfg *config.Config, logger *slog.Logger, deps commandDependencies) (storage.ObjectStore, error) {
|
||||
|
||||
@@ -24,8 +24,7 @@ func TestNewCommandObjectStoreLoadsSecretsBeforeFactory(t *testing.T) {
|
||||
cfg := commandObjectStoreTestConfig(secretsDir)
|
||||
fake := &storage.FakeBackend{}
|
||||
called := false
|
||||
origStoreFn := newObjectStoreFromConfigFn
|
||||
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
deps := commandDependencies{newObjectStore: func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
called = true
|
||||
if got := os.Getenv(accessKeyEnv); got != "loaded-key-id" {
|
||||
return nil, errors.New("access key was not loaded before object store init")
|
||||
@@ -34,12 +33,9 @@ func TestNewCommandObjectStoreLoadsSecretsBeforeFactory(t *testing.T) {
|
||||
return nil, errors.New("secret key was not loaded before object store init")
|
||||
}
|
||||
return fake, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
newObjectStoreFromConfigFn = origStoreFn
|
||||
})
|
||||
}}
|
||||
|
||||
store, err := newCommandObjectStore(context.Background(), cfg, nil)
|
||||
store, err := newCommandObjectStoreWithDependencies(context.Background(), cfg, nil, deps)
|
||||
if err != nil {
|
||||
t.Fatalf("newCommandObjectStore() error = %v", err)
|
||||
}
|
||||
@@ -62,8 +58,7 @@ func TestNewCommandObjectStorePreservesExistingEnv(t *testing.T) {
|
||||
mustWriteSecretFile(t, filepath.Join(secretsDir, secretKeyEnv), "file-secret\n")
|
||||
|
||||
cfg := commandObjectStoreTestConfig(secretsDir)
|
||||
origStoreFn := newObjectStoreFromConfigFn
|
||||
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
deps := commandDependencies{newObjectStore: func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
if got := os.Getenv(accessKeyEnv); got != "existing-key-id" {
|
||||
return nil, errors.New("existing access key was overwritten")
|
||||
}
|
||||
@@ -71,12 +66,9 @@ func TestNewCommandObjectStorePreservesExistingEnv(t *testing.T) {
|
||||
return nil, errors.New("existing secret key was overwritten")
|
||||
}
|
||||
return &storage.FakeBackend{}, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
newObjectStoreFromConfigFn = origStoreFn
|
||||
})
|
||||
}}
|
||||
|
||||
if _, err := newCommandObjectStore(context.Background(), cfg, nil); err != nil {
|
||||
if _, err := newCommandObjectStoreWithDependencies(context.Background(), cfg, nil, deps); err != nil {
|
||||
t.Fatalf("newCommandObjectStore() error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -84,16 +76,12 @@ func TestNewCommandObjectStorePreservesExistingEnv(t *testing.T) {
|
||||
func TestNewCommandObjectStoreSecretErrorStopsFactory(t *testing.T) {
|
||||
cfg := commandObjectStoreTestConfig(filepath.Join(t.TempDir(), "missing"))
|
||||
called := false
|
||||
origStoreFn := newObjectStoreFromConfigFn
|
||||
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
deps := commandDependencies{newObjectStore: func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
called = true
|
||||
return &storage.FakeBackend{}, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
newObjectStoreFromConfigFn = origStoreFn
|
||||
})
|
||||
}}
|
||||
|
||||
_, err := newCommandObjectStore(context.Background(), cfg, nil)
|
||||
_, err := newCommandObjectStoreWithDependencies(context.Background(), cfg, nil, deps)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
@@ -107,15 +95,11 @@ func TestNewCommandObjectStoreSecretErrorStopsFactory(t *testing.T) {
|
||||
|
||||
func TestNewCommandObjectStoreFactoryErrorIsContextual(t *testing.T) {
|
||||
cfg := commandObjectStoreTestConfig("")
|
||||
origStoreFn := newObjectStoreFromConfigFn
|
||||
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
deps := commandDependencies{newObjectStore: func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
return nil, errors.New("factory boom")
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
newObjectStoreFromConfigFn = origStoreFn
|
||||
})
|
||||
}}
|
||||
|
||||
_, err := newCommandObjectStore(context.Background(), cfg, nil)
|
||||
_, err := newCommandObjectStoreWithDependencies(context.Background(), cfg, nil, deps)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ import (
|
||||
|
||||
// ArtifactsList lists effective artifact sources.
|
||||
func ArtifactsList(ctx context.Context, args []string, out io.Writer) error {
|
||||
return artifactsListWithDependencies(ctx, args, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func artifactsListWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error {
|
||||
deps = normalizeCommandDependencies(deps)
|
||||
fs := flag.NewFlagSet("artifacts list", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
@@ -22,7 +27,7 @@ func ArtifactsList(ctx context.Context, args []string, out io.Writer) error {
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("artifacts list: session_id is required")
|
||||
}
|
||||
cfg, store, locks, m, cleanup, err := loadHelperContext(ctx, flags, remote)
|
||||
cfg, store, locks, m, cleanup, err := loadHelperContextWithDependencies(ctx, flags, remote, deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("artifacts list: %w", err)
|
||||
}
|
||||
@@ -33,7 +38,7 @@ func ArtifactsList(ctx context.Context, args []string, out io.Writer) error {
|
||||
}
|
||||
publishedRemoteState := map[string]string{}
|
||||
if remote && store != nil {
|
||||
current := inspectRemoteCurrentState(ctx, cfg, store)
|
||||
current := inspectRemoteCurrentStateWithDependencies(ctx, cfg, store, deps)
|
||||
if current.Err != nil {
|
||||
publishedRemoteState = unresolvedRemotePublishedOutputAvailability(cfg, catalog, "remote=error")
|
||||
} else {
|
||||
|
||||
@@ -47,20 +47,19 @@ func (f commonConfigFlags) sessionOptions() config.SessionLoadOptions {
|
||||
|
||||
// Session dispatches session helper subcommands.
|
||||
func Session(ctx context.Context, args []string, out io.Writer) error {
|
||||
return sessionWithDependencies(ctx, args, out, commandDependencies{
|
||||
newObjectStore: newObjectStoreFromConfigFn,
|
||||
})
|
||||
return sessionWithDependencies(ctx, args, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func sessionWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error {
|
||||
deps = normalizeCommandDependencies(deps)
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("session: expected subcommand: init|validate|status|plan|restore|artifacts|locks")
|
||||
}
|
||||
switch args[0] {
|
||||
case "init":
|
||||
return SessionInit(ctx, args[1:], out)
|
||||
return sessionInitWithDependencies(ctx, args[1:], out, deps)
|
||||
case "validate":
|
||||
return SessionValidate(ctx, args[1:], out)
|
||||
return sessionValidateWithDependencies(ctx, args[1:], out, deps)
|
||||
case "status":
|
||||
return statusWithDependencies(ctx, args[1:], out, deps)
|
||||
case "plan":
|
||||
@@ -68,9 +67,9 @@ func sessionWithDependencies(ctx context.Context, args []string, out io.Writer,
|
||||
case "restore":
|
||||
return Restore(ctx, args[1:], out)
|
||||
case "artifacts":
|
||||
return ArtifactsList(ctx, args[1:], out)
|
||||
return artifactsListWithDependencies(ctx, args[1:], out, deps)
|
||||
case "locks":
|
||||
return SessionLocks(ctx, args[1:], out)
|
||||
return sessionLocksWithDependencies(ctx, args[1:], out, deps)
|
||||
default:
|
||||
return fmt.Errorf("session: unknown subcommand %q", args[0])
|
||||
}
|
||||
@@ -79,32 +78,45 @@ func sessionWithDependencies(ctx context.Context, args []string, out io.Writer,
|
||||
// SessionLocks dispatches session-oriented publish lock list and mutation
|
||||
// helpers.
|
||||
func SessionLocks(ctx context.Context, args []string, out io.Writer) error {
|
||||
return sessionLocksWithDependencies(ctx, args, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func sessionLocksWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error {
|
||||
if len(args) > 0 && !isCLIFlagToken(args[0]) {
|
||||
switch args[0] {
|
||||
case "add":
|
||||
return LocksAdd(ctx, args[1:], out)
|
||||
return locksAddWithDependencies(ctx, args[1:], out, deps)
|
||||
case "remove":
|
||||
return LocksRemove(ctx, args[1:], out)
|
||||
return locksRemoveWithDependencies(ctx, args[1:], out, deps)
|
||||
}
|
||||
}
|
||||
return LocksList(ctx, args, out)
|
||||
return locksListWithDependencies(ctx, args, out, deps)
|
||||
}
|
||||
|
||||
// Artifacts dispatches artifact helper subcommands.
|
||||
func Artifacts(ctx context.Context, args []string, out io.Writer) error {
|
||||
return artifactsWithDependencies(ctx, args, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func artifactsWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error {
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("artifacts: expected subcommand: list")
|
||||
}
|
||||
switch args[0] {
|
||||
case "list":
|
||||
return ArtifactsList(ctx, args[1:], out)
|
||||
return artifactsListWithDependencies(ctx, args[1:], out, deps)
|
||||
default:
|
||||
return fmt.Errorf("artifacts: unknown subcommand %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func loadHelperContext(ctx context.Context, flags commonConfigFlags, needStore bool) (*config.Config, storage.ObjectStore, []artifacts.RemoteLock, *manifest.Manifest, func(), error) {
|
||||
loaded, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions())
|
||||
return loadHelperContextWithDependencies(ctx, flags, needStore, commandDependencies{})
|
||||
}
|
||||
|
||||
func loadHelperContextWithDependencies(ctx context.Context, flags commonConfigFlags, needStore bool, deps commandDependencies) (*config.Config, storage.ObjectStore, []artifacts.RemoteLock, *manifest.Manifest, func(), error) {
|
||||
deps = normalizeCommandDependencies(deps)
|
||||
loaded, err := loadCommandConfigWithDependencies(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions(), deps)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
@@ -120,12 +132,12 @@ func loadHelperContext(ctx context.Context, flags commonConfigFlags, needStore b
|
||||
}
|
||||
var store storage.ObjectStore
|
||||
if needStore {
|
||||
store, err = newCommandObjectStore(ctx, cfg, nil)
|
||||
store, err = newCommandObjectStoreWithDependencies(ctx, cfg, nil, deps)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
} else {
|
||||
store, _ = objectStoreIfConfigured(ctx, cfg)
|
||||
store, _ = objectStoreIfConfiguredWithDependencies(ctx, cfg, deps)
|
||||
}
|
||||
var locks []artifacts.RemoteLock
|
||||
if store != nil {
|
||||
@@ -148,9 +160,7 @@ func loadHelperContext(ctx context.Context, flags commonConfigFlags, needStore b
|
||||
}
|
||||
|
||||
func objectStoreIfConfigured(ctx context.Context, cfg *config.Config) (storage.ObjectStore, error) {
|
||||
return objectStoreIfConfiguredWithDependencies(ctx, cfg, commandDependencies{
|
||||
newObjectStore: newObjectStoreFromConfigFn,
|
||||
})
|
||||
return objectStoreIfConfiguredWithDependencies(ctx, cfg, commandDependencies{})
|
||||
}
|
||||
|
||||
func objectStoreIfConfiguredWithDependencies(ctx context.Context, cfg *config.Config, deps commandDependencies) (storage.ObjectStore, error) {
|
||||
|
||||
@@ -23,17 +23,17 @@ func TestExecuteSessionInitRemoteWritesCanonicalSessionConfig(t *testing.T) {
|
||||
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{filepath.Join(t.TempDir(), "session.yml")})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "init", "2026-06-07",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--title", "The Black Cabin",
|
||||
"--remote",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -56,14 +56,14 @@ func TestExecuteSessionInitRemoteUsesDefaultConfigDiscovery(t *testing.T) {
|
||||
withDefaultPipelineCampaignConfigs(t, pipelinePath, campaignPath)
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{filepath.Join(t.TempDir(), "session.yml")})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "init", "2026-06-07",
|
||||
"--remote",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -124,16 +124,16 @@ inputs:
|
||||
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{filepath.Join(t.TempDir(), "session.yml")})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "init", "2026-06-07",
|
||||
"--config", defaultPipeline,
|
||||
"--campaign-file", explicitCampaign,
|
||||
"--remote",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -194,20 +194,16 @@ func TestExecuteSessionInitRemoteLoadsSecretsBeforeObjectStoreInit(t *testing.T)
|
||||
addSecretsToPipelineConfig(t, pipelinePath, secretsDir, accessKeyEnv, secretKeyEnv)
|
||||
|
||||
fake := &storage.FakeBackend{}
|
||||
origStoreFn := newObjectStoreFromConfigFn
|
||||
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
deps := commandDependencies{newObjectStore: func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
if os.Getenv(accessKeyEnv) != "test-key-id" || os.Getenv(secretKeyEnv) != "test-secret" {
|
||||
return nil, fmt.Errorf("secrets were not loaded before object store init")
|
||||
}
|
||||
return fake, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
newObjectStoreFromConfigFn = origStoreFn
|
||||
})
|
||||
}}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{"session", "init", "2026-06-07", "--remote"}, &stdout, &stderr)
|
||||
code := executeWithDependencies([]string{"session", "init", "2026-06-07", "--remote"}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -272,16 +268,16 @@ inputs:
|
||||
`)
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{filepath.Join(t.TempDir(), "session.yml")})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "init", "2026-06-07",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--remote",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -431,20 +427,16 @@ inputs:
|
||||
fake := &storage.FakeBackend{}
|
||||
audioKey := artifacts.S3PublishedOutputKey(artifacts.S3AudioPrefix(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "audio/"), "alice.flac")
|
||||
fake.SeedObject(storage.FakeObject{Key: audioKey, Data: []byte("audio")})
|
||||
origStoreFn := newObjectStoreFromConfigFn
|
||||
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
deps := commandDependencies{newObjectStore: func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
if os.Getenv(accessKeyEnv) != "test-key-id" || os.Getenv(secretKeyEnv) != "test-secret" {
|
||||
return nil, fmt.Errorf("secrets were not loaded before object store init")
|
||||
}
|
||||
return fake, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
newObjectStoreFromConfigFn = origStoreFn
|
||||
})
|
||||
}}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{"session", "validate", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
||||
code := executeWithDependencies([]string{"session", "validate", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||
}
|
||||
@@ -472,7 +464,7 @@ func TestOperatorCommandsReportConfiguredSpellCatalog(t *testing.T) {
|
||||
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
commonArgs := []string{
|
||||
"2026-05-03",
|
||||
"--config", pipelinePath,
|
||||
@@ -483,7 +475,7 @@ func TestOperatorCommandsReportConfiguredSpellCatalog(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
validateArgs := append([]string{"session", "validate"}, commonArgs...)
|
||||
if code := Execute(validateArgs, &stdout, &stderr); code != 0 {
|
||||
if code := executeWithDependencies(validateArgs, &stdout, &stderr, deps, executePlan); code != 0 {
|
||||
t.Fatalf("validate exit code = %d, want 0; stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "OK inputs spell_catalog: "+spellPath) {
|
||||
@@ -493,7 +485,7 @@ func TestOperatorCommandsReportConfiguredSpellCatalog(t *testing.T) {
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
statusArgs := append([]string{"session", "status"}, commonArgs...)
|
||||
if code := executeWithDependencies(statusArgs, &stdout, &stderr, fakeCommandDependencies(fake, &storeInitCalls), executePlan); code != 0 {
|
||||
if code := executeWithDependencies(statusArgs, &stdout, &stderr, deps, executePlan); code != 0 {
|
||||
t.Fatalf("status exit code = %d, want 0; stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Stable input spell_catalog: "+spellPath) {
|
||||
@@ -505,7 +497,7 @@ func TestOperatorCommandsReportConfiguredSpellCatalog(t *testing.T) {
|
||||
}
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
if code := Execute(validateArgs, &stdout, &stderr); code == 0 {
|
||||
if code := executeWithDependencies(validateArgs, &stdout, &stderr, deps, executePlan); code == 0 {
|
||||
t.Fatalf("validate missing spell catalog exit code = 0; stdout=%q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "ERROR inputs spell_catalog missing:") {
|
||||
@@ -518,17 +510,17 @@ func TestExecuteLocksAddListAndRemoveUseIndependentObjects(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--reason", "manual edit",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("locks add exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -547,12 +539,12 @@ func TestExecuteLocksAddListAndRemoveUseIndependentObjects(t *testing.T) {
|
||||
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
code = Execute([]string{
|
||||
code = executeWithDependencies([]string{
|
||||
"session", "locks", "2026-05-03",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("locks list exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -562,12 +554,12 @@ func TestExecuteLocksAddListAndRemoveUseIndependentObjects(t *testing.T) {
|
||||
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
code = Execute([]string{
|
||||
code = executeWithDependencies([]string{
|
||||
"session", "locks", "remove", "2026-05-03", "narratio.transcript.final_trimmed",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -576,12 +568,12 @@ func TestExecuteLocksAddListAndRemoveUseIndependentObjects(t *testing.T) {
|
||||
}
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
code = Execute([]string{
|
||||
code = executeWithDependencies([]string{
|
||||
"session", "locks", "remove", "2026-05-03", "narratio.transcript.final_trimmed",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 || !strings.Contains(stdout.String(), "was not locked") {
|
||||
t.Fatalf("idempotent remove exit code = %d, stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||
}
|
||||
@@ -595,7 +587,7 @@ func TestExecuteLocksListIsSortedAndRejectsMalformedObjects(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
|
||||
seedOperatorRemoteLock(t, fake, sessionPrefix, "narratio.transcript.final_trimmed", "second")
|
||||
seedOperatorRemoteLock(t, fake, sessionPrefix, "narratio.transcript.final", "first")
|
||||
@@ -603,7 +595,7 @@ func TestExecuteLocksListIsSortedAndRejectsMalformedObjects(t *testing.T) {
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
if code := Execute(args, &stdout, &stderr); code != 0 {
|
||||
if code := executeWithDependencies(args, &stdout, &stderr, deps, executePlan); code != 0 {
|
||||
t.Fatalf("locks list exit code = %d; stderr=%q", code, stderr.String())
|
||||
}
|
||||
first := strings.Index(stdout.String(), "narratio.transcript.final ")
|
||||
@@ -619,7 +611,7 @@ func TestExecuteLocksListIsSortedAndRejectsMalformedObjects(t *testing.T) {
|
||||
fake.SeedObject(storage.FakeObject{Key: badKey, Data: []byte(`{"format_version":1}`)})
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
if code := Execute(args, &stdout, &stderr); code == 0 {
|
||||
if code := executeWithDependencies(args, &stdout, &stderr, deps, executePlan); code == 0 {
|
||||
t.Fatalf("malformed lock list exit code = 0; stdout=%q", stdout.String())
|
||||
}
|
||||
}
|
||||
@@ -629,30 +621,30 @@ func TestExecuteLocksAddDuplicateRequiresForce(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--reason", "first",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("initial locks add exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
code = Execute([]string{
|
||||
code = executeWithDependencies([]string{
|
||||
"session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--reason", "second",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code == 0 {
|
||||
t.Fatal("duplicate locks add exit code = 0, want non-zero")
|
||||
}
|
||||
@@ -662,14 +654,14 @@ func TestExecuteLocksAddDuplicateRequiresForce(t *testing.T) {
|
||||
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
code = Execute([]string{
|
||||
code = executeWithDependencies([]string{
|
||||
"session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--reason", "second",
|
||||
"--force",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("forced locks add exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -688,7 +680,7 @@ func TestExecuteLocksForceReportsConcurrentReplacement(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
|
||||
key := seedOperatorRemoteLock(t, fake, sessionPrefix, "narratio.transcript.final_trimmed", "first")
|
||||
replacement, err := artifacts.NewRemoteLock("narratio.transcript.final_trimmed", "concurrent", time.Date(2026, 5, 20, 1, 2, 3, 0, time.UTC))
|
||||
@@ -708,10 +700,10 @@ func TestExecuteLocksForceReportsConcurrentReplacement(t *testing.T) {
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed", "--force",
|
||||
"--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath,
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code == 0 || !strings.Contains(stderr.String(), "changed concurrently; retry") {
|
||||
t.Fatalf("forced conflict exit code = %d, stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -747,7 +739,7 @@ func TestExecuteLocksMutationRejectsSessionIDMismatch(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -778,7 +770,7 @@ func TestExecuteLocksMutationRejectsSessionIDMismatch(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute(tt.args, &stdout, &stderr)
|
||||
code := executeWithDependencies(tt.args, &stdout, &stderr, deps, executePlan)
|
||||
if code == 0 {
|
||||
t.Fatal("exit code = 0, want non-zero")
|
||||
}
|
||||
@@ -903,17 +895,17 @@ func TestExecuteArtifactsListRemoteReportsPublishedAvailability(t *testing.T) {
|
||||
"narratio.transcript.final_trimmed": "transcripts/final.trimmed.json",
|
||||
})
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "artifacts", "2026-05-03",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--remote",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -938,17 +930,17 @@ func TestExecuteArtifactsListReportsExtractionLifecycleWithoutPayload(t *testing
|
||||
"narratio.extraction.encounters": "artifacts/encounters.json",
|
||||
})
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "artifacts", "2026-05-03",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--remote",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -971,12 +963,12 @@ func TestExecuteArtifactsListReportsExtractionLifecycleWithoutPayload(t *testing
|
||||
}
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
code = Execute([]string{
|
||||
code = executeWithDependencies([]string{
|
||||
"session", "artifacts", "2026-05-03",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("unavailable exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -1003,17 +995,17 @@ func TestExecuteArtifactsListRemoteUsesPublishOutputDestinations(t *testing.T) {
|
||||
"narratio.bounds.session": "transcripts/bounds.json",
|
||||
})
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "artifacts", "2026-05-03",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--remote",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -1056,7 +1048,6 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
|
||||
})
|
||||
seedOperatorRemoteLock(t, fake, sessionPrefix, "narratio.transcript.final_trimmed", "remote review")
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
@@ -1103,7 +1094,6 @@ func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testin
|
||||
`)
|
||||
fake := &storage.FakeBackend{ExistsErr: fmt.Errorf("exists failed")}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
@@ -1131,7 +1121,6 @@ func TestExecuteStatusReportsMissingRemoteCurrentStateWithoutFailing(t *testing.
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
@@ -1157,7 +1146,6 @@ func TestExecuteStatusReportsPreviousStateReadinessWithoutFailing(t *testing.T)
|
||||
replaceInFileOrFatal(t, sessionPath, "session_id: 2026-05-03\n", "session_id: 2026-05-03\nprevious_session_id: 2026-04-26\n")
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
@@ -1188,7 +1176,6 @@ func TestExecuteStatusDetectsMissingRequiredPreviousArtifact(t *testing.T) {
|
||||
fake := &storage.FakeBackend{}
|
||||
seedRestorePreviousCurrentManifestOnly(t, fake, cfg)
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
@@ -1214,16 +1201,16 @@ func TestExecuteSessionValidateReportsPreviousStateFindingAndReturnsFindingError
|
||||
replaceInFileOrFatal(t, sessionPath, "session_id: 2026-05-03\n", "session_id: 2026-05-03\nprevious_session_id: 2026-04-26\n")
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "validate", "2026-05-03",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code == 0 {
|
||||
t.Fatal("exit code = 0, want non-zero")
|
||||
}
|
||||
|
||||
@@ -184,9 +184,7 @@ func inspectPreviousArtifactReadiness(
|
||||
}
|
||||
|
||||
func inspectRemoteCurrentState(ctx context.Context, cfg *config.Config, store storage.ObjectStore) remoteCurrentStateCheck {
|
||||
return inspectRemoteCurrentStateWithDependencies(ctx, cfg, store, commandDependencies{
|
||||
discoverRemoteCurrentState: discoverRemoteCurrentStateFn,
|
||||
})
|
||||
return inspectRemoteCurrentStateWithDependencies(ctx, cfg, store, commandDependencies{})
|
||||
}
|
||||
|
||||
func inspectRemoteCurrentStateWithDependencies(ctx context.Context, cfg *config.Config, store storage.ObjectStore, deps commandDependencies) remoteCurrentStateCheck {
|
||||
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
|
||||
// LocksList lists independent publish locks.
|
||||
func LocksList(ctx context.Context, args []string, out io.Writer) error {
|
||||
return locksListWithDependencies(ctx, args, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func locksListWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error {
|
||||
fs := flag.NewFlagSet("locks", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
@@ -26,7 +30,7 @@ func LocksList(ctx context.Context, args []string, out io.Writer) error {
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("locks: session_id is required")
|
||||
}
|
||||
cfg, store, sessionPrefix, cleanup, err := loadLockCommandContext(ctx, flags)
|
||||
cfg, store, sessionPrefix, cleanup, err := loadLockCommandContextWithDependencies(ctx, flags, deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("locks: %w", err)
|
||||
}
|
||||
@@ -41,6 +45,10 @@ func LocksList(ctx context.Context, args []string, out io.Writer) error {
|
||||
|
||||
// LocksAdd adds or updates one remote lock.
|
||||
func LocksAdd(ctx context.Context, args []string, out io.Writer) error {
|
||||
return locksAddWithDependencies(ctx, args, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func locksAddWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error {
|
||||
fs := flag.NewFlagSet("locks add", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
@@ -56,7 +64,7 @@ func LocksAdd(ctx context.Context, args []string, out io.Writer) error {
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("locks add: session_id is required")
|
||||
}
|
||||
_, store, sessionPrefix, cleanup, err := loadLockCommandContext(ctx, flags)
|
||||
_, store, sessionPrefix, cleanup, err := loadLockCommandContextWithDependencies(ctx, flags, deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("locks add: %w", err)
|
||||
}
|
||||
@@ -86,6 +94,10 @@ func LocksAdd(ctx context.Context, args []string, out io.Writer) error {
|
||||
|
||||
// LocksRemove removes one remote lock.
|
||||
func LocksRemove(ctx context.Context, args []string, out io.Writer) error {
|
||||
return locksRemoveWithDependencies(ctx, args, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func locksRemoveWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error {
|
||||
fs := flag.NewFlagSet("locks remove", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
@@ -97,7 +109,7 @@ func LocksRemove(ctx context.Context, args []string, out io.Writer) error {
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("locks remove: session_id is required")
|
||||
}
|
||||
_, store, sessionPrefix, cleanup, err := loadLockCommandContext(ctx, flags)
|
||||
_, store, sessionPrefix, cleanup, err := loadLockCommandContextWithDependencies(ctx, flags, deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("locks remove: %w", err)
|
||||
}
|
||||
@@ -118,7 +130,12 @@ func LocksRemove(ctx context.Context, args []string, out io.Writer) error {
|
||||
}
|
||||
|
||||
func loadLockCommandContext(ctx context.Context, flags commonConfigFlags) (*config.Config, storage.ObjectStore, string, func(), error) {
|
||||
loaded, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions())
|
||||
return loadLockCommandContextWithDependencies(ctx, flags, commandDependencies{})
|
||||
}
|
||||
|
||||
func loadLockCommandContextWithDependencies(ctx context.Context, flags commonConfigFlags, deps commandDependencies) (*config.Config, storage.ObjectStore, string, func(), error) {
|
||||
deps = normalizeCommandDependencies(deps)
|
||||
loaded, err := loadCommandConfigWithDependencies(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions(), deps)
|
||||
if err != nil {
|
||||
return nil, nil, "", nil, err
|
||||
}
|
||||
@@ -132,7 +149,7 @@ func loadLockCommandContext(ctx context.Context, flags commonConfigFlags) (*conf
|
||||
if err := config.Validate(cfg); err != nil {
|
||||
return nil, nil, "", nil, err
|
||||
}
|
||||
store, err := newCommandObjectStore(ctx, cfg, nil)
|
||||
store, err := newCommandObjectStoreWithDependencies(ctx, cfg, nil, deps)
|
||||
if err != nil {
|
||||
return nil, nil, "", nil, err
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ import (
|
||||
|
||||
// SessionInit creates a local or remote session.yml skeleton.
|
||||
func SessionInit(ctx context.Context, args []string, out io.Writer) error {
|
||||
return sessionInitWithDependencies(ctx, args, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func sessionInitWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error {
|
||||
deps = normalizeCommandDependencies(deps)
|
||||
fs := flag.NewFlagSet("session init", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var pipelinePath, campaignPath, campaignFilePath, sessionID, previousSessionID, date, title, output, audioS3Prefix, audioDir string
|
||||
@@ -51,7 +56,7 @@ func SessionInit(ctx context.Context, args []string, out io.Writer) error {
|
||||
return fmt.Errorf("session init: --audio-dir and --audio-s3-prefix are mutually exclusive")
|
||||
}
|
||||
|
||||
base, err := loadPipelineCampaignConfig(pipelinePath, campaignPath, campaignFilePath, config.PipelineLoadOptions{Profile: profile.pointer()})
|
||||
base, err := loadPipelineCampaignConfigWithDependencies(pipelinePath, campaignPath, campaignFilePath, config.PipelineLoadOptions{Profile: profile.pointer()}, deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("session init: %w", err)
|
||||
}
|
||||
@@ -98,7 +103,7 @@ func SessionInit(ctx context.Context, args []string, out io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
store, err := newCommandObjectStore(ctx, cfg, nil)
|
||||
store, err := newCommandObjectStoreWithDependencies(ctx, cfg, nil, deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("session init: %w", err)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ import (
|
||||
|
||||
// SessionValidate performs a read-only session preflight.
|
||||
func SessionValidate(ctx context.Context, args []string, out io.Writer) error {
|
||||
return sessionValidateWithDependencies(ctx, args, out, commandDependencies{})
|
||||
}
|
||||
|
||||
func sessionValidateWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error {
|
||||
deps = normalizeCommandDependencies(deps)
|
||||
fs := flag.NewFlagSet("session validate", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
@@ -25,7 +30,7 @@ func SessionValidate(ctx context.Context, args []string, out io.Writer) error {
|
||||
}
|
||||
|
||||
findings := []finding{}
|
||||
loaded, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions())
|
||||
loaded, err := loadCommandConfigWithDependencies(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions(), deps)
|
||||
if err != nil {
|
||||
findings = append(findings, errorFinding("config", err.Error()))
|
||||
return renderFindings(out, "", "", findings)
|
||||
@@ -43,7 +48,7 @@ func SessionValidate(ctx context.Context, args []string, out io.Writer) error {
|
||||
findings = append(findings, validateStableInputFindings(cfg)...)
|
||||
findings = append(findings, validateLocalAudioFindings(cfg)...)
|
||||
|
||||
store, storeErr := objectStoreIfConfigured(ctx, cfg)
|
||||
store, storeErr := objectStoreIfConfiguredWithDependencies(ctx, cfg, deps)
|
||||
if storeErr != nil {
|
||||
findings = append(findings, errorFinding("storage", storeErr.Error()))
|
||||
}
|
||||
|
||||
@@ -426,23 +426,6 @@ func TestExecuteRemoteSessionMismatchFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func restoreAppConfigTestGlobals(t *testing.T, fake *storage.FakeBackend, storeInitCalls *int, sessionDefaults []string) {
|
||||
t.Helper()
|
||||
origStoreFn := newObjectStoreFromConfigFn
|
||||
origSessionDefaults := append([]string(nil), config.DefaultSessionConfigSearchPaths...)
|
||||
config.DefaultSessionConfigSearchPaths = append([]string(nil), sessionDefaults...)
|
||||
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
|
||||
if storeInitCalls != nil {
|
||||
(*storeInitCalls)++
|
||||
}
|
||||
return fake, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
newObjectStoreFromConfigFn = origStoreFn
|
||||
config.DefaultSessionConfigSearchPaths = origSessionDefaults
|
||||
})
|
||||
}
|
||||
|
||||
func remoteSessionLoaderDependencies(fake *storage.FakeBackend) (commandDependencies, *int) {
|
||||
storeInitCalls := new(int)
|
||||
return fakeCommandDependencies(fake, storeInitCalls), storeInitCalls
|
||||
|
||||
@@ -54,7 +54,8 @@ func Restore(ctx context.Context, args []string, out io.Writer) (resultErr error
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("restore: session_id is required")
|
||||
}
|
||||
loaded, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions())
|
||||
restoreDeps := commandDependencies{newObjectStore: newObjectStoreFromConfigFn}
|
||||
loaded, err := loadCommandConfigWithDependencies(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions(), restoreDeps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("restore: %w", err)
|
||||
}
|
||||
@@ -63,7 +64,7 @@ func Restore(ctx context.Context, args []string, out io.Writer) (resultErr error
|
||||
if err := config.Validate(cfg); err != nil {
|
||||
return fmt.Errorf("restore: %w", err)
|
||||
}
|
||||
objectStore, err := newCommandObjectStore(ctx, cfg, logging.NewLogger(os.Stderr, slog.LevelInfo))
|
||||
objectStore, err := newCommandObjectStoreWithDependencies(ctx, cfg, logging.NewLogger(os.Stderr, slog.LevelInfo), restoreDeps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("restore: %w", err)
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ func TestExecuteSessionSubcommandsAcceptPositionalSessionID(t *testing.T) {
|
||||
fake.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
fake.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "2026-05-03", "sample-campaign")})
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -270,7 +270,7 @@ func TestExecuteSessionSubcommandsAcceptPositionalSessionID(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute(tt.args, &stdout, &stderr)
|
||||
code := executeWithDependencies(tt.args, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||
}
|
||||
@@ -311,17 +311,17 @@ func TestExecuteSessionLocksMutationAcceptsPositionalSessionID(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
deps := fakeCommandDependencies(fake, &storeInitCalls)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
code := executeWithDependencies([]string{
|
||||
"session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--reason", "review",
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("locks add exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
@@ -336,12 +336,12 @@ func TestExecuteSessionLocksMutationAcceptsPositionalSessionID(t *testing.T) {
|
||||
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
code = Execute([]string{
|
||||
code = executeWithDependencies([]string{
|
||||
"session", "locks", "remove", "2026-05-03", "narratio.transcript.final_trimmed",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
}, &stdout, &stderr)
|
||||
}, &stdout, &stderr, deps, executePlan)
|
||||
if code != 0 {
|
||||
t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user