Carry dependencies through operator commands

This commit is contained in:
2026-09-05 18:00:50 +00:00
parent dd905c4daa
commit f51b9bb046
17 changed files with 182 additions and 172 deletions

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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())
}

View File

@@ -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:

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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")
}

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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")
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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()))
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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())
}