Extract shared read-only session inspection checks
This commit is contained in:
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
@@ -64,26 +63,18 @@ func sessionSourceSummary(cfg *config.Config) string {
|
||||
}
|
||||
|
||||
func validateStableInputFindings(cfg *config.Config) []finding {
|
||||
items := []struct {
|
||||
name string
|
||||
in config.ResolvedInputFile
|
||||
}{
|
||||
{"speakers", cfg.StableInputs.SpeakersFile},
|
||||
{"autocorrect", cfg.StableInputs.AutocorrectFile},
|
||||
{"glossary", cfg.StableInputs.GlossaryFile},
|
||||
}
|
||||
out := make([]finding, 0, len(items))
|
||||
for _, item := range items {
|
||||
path, err := resolveHelperConfigRelativePath(item.in)
|
||||
if err != nil {
|
||||
out = append(out, errorFinding("inputs", item.name+": "+err.Error()))
|
||||
checks := inspectStableInputs(cfg)
|
||||
out := make([]finding, 0, len(checks))
|
||||
for _, check := range checks {
|
||||
if check.Err != nil {
|
||||
msg := check.Name + ": " + check.Err.Error()
|
||||
if strings.TrimSpace(check.Path) != "" {
|
||||
msg = fmt.Sprintf("%s missing: %v", check.Name, check.Err)
|
||||
}
|
||||
out = append(out, errorFinding("inputs", msg))
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
out = append(out, errorFinding("inputs", fmt.Sprintf("%s missing: %v", item.name, err)))
|
||||
} else {
|
||||
out = append(out, okFinding("inputs", item.name+": "+path))
|
||||
}
|
||||
out = append(out, okFinding("inputs", check.Name+": "+check.Path))
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -103,76 +94,22 @@ func resolveHelperConfigRelativePath(input config.ResolvedInputFile) (string, er
|
||||
}
|
||||
|
||||
func validateLocalAudioFindings(cfg *config.Config) []finding {
|
||||
if cfg.Session.Inputs.AudioS3 != nil {
|
||||
check := inspectLocalAudioPresence(cfg)
|
||||
if !check.Checked {
|
||||
return nil
|
||||
}
|
||||
audioDir := strings.TrimSpace(cfg.Session.Inputs.AudioDir)
|
||||
if audioDir == "" && len(cfg.Session.Inputs.AudioFiles) == 0 {
|
||||
return []finding{errorFinding("audio", "audio_dir, audio_files, or audio_s3 is required")}
|
||||
if check.Err != nil {
|
||||
return []finding{errorFinding("audio", check.Err.Error())}
|
||||
}
|
||||
base := filepath.Dir(cfg.SessionPath)
|
||||
paths := []string{}
|
||||
if audioDir != "" {
|
||||
dir := audioDir
|
||||
if !filepath.IsAbs(dir) {
|
||||
dir = filepath.Join(base, dir)
|
||||
}
|
||||
matches, err := filepath.Glob(filepath.Join(dir, "*.flac"))
|
||||
if err != nil || len(matches) == 0 {
|
||||
return []finding{errorFinding("audio", "no .flac files found in "+dir)}
|
||||
}
|
||||
paths = append(paths, matches...)
|
||||
}
|
||||
for _, file := range cfg.Session.Inputs.AudioFiles {
|
||||
p := file
|
||||
if !filepath.IsAbs(p) {
|
||||
p = filepath.Join(base, p)
|
||||
}
|
||||
paths = append(paths, p)
|
||||
}
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
return []finding{errorFinding("audio", fmt.Sprintf("audio file missing: %v", err))}
|
||||
}
|
||||
}
|
||||
return []finding{okFinding("audio", fmt.Sprintf("%d local audio file(s)", len(paths)))}
|
||||
return []finding{okFinding("audio", fmt.Sprintf("%d local audio file(s)", len(check.Paths)))}
|
||||
}
|
||||
|
||||
func validateRemoteAudioFinding(ctx context.Context, cfg *config.Config, store storage.ObjectStore) finding {
|
||||
sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
audioPrefix := artifacts.S3AudioPrefix(sessionPrefix, cfg.Session.Inputs.AudioS3.Prefix)
|
||||
objects, err := store.List(ctx, audioPrefix)
|
||||
if err != nil {
|
||||
return errorFinding("audio", err.Error())
|
||||
check := inspectRemoteAudioPresence(ctx, cfg, store)
|
||||
if check.Err != nil {
|
||||
return errorFinding("audio", check.Err.Error())
|
||||
}
|
||||
count := 0
|
||||
for _, obj := range objects {
|
||||
if strings.HasSuffix(strings.ToLower(obj.Key), ".flac") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
return errorFinding("audio", "no remote .flac objects found under "+audioPrefix)
|
||||
}
|
||||
return okFinding("audio", fmt.Sprintf("%d remote .flac object(s)", count))
|
||||
}
|
||||
|
||||
func validatePreviousArtifactFindings(ctx context.Context, cfg *config.Config, store storage.ObjectStore, requirements []artifacts.PreviousArtifactRequirement) []finding {
|
||||
out := []finding{}
|
||||
prefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID)
|
||||
_, err := artifacts.LoadCurrentState(ctx, store, prefix, artifacts.CurrentStateValidation{
|
||||
ExpectedSessionID: strings.TrimSpace(cfg.Session.PreviousSessionID),
|
||||
ExpectedCampaign: strings.TrimSpace(cfg.Session.Campaign),
|
||||
ValidateRunID: true,
|
||||
})
|
||||
if err != nil {
|
||||
out = append(out, errorFinding("previous", fmt.Sprintf("remote %v", err)))
|
||||
return out
|
||||
}
|
||||
for _, req := range requirements {
|
||||
out = append(out, okFinding("previous", fmt.Sprintf("%s required=%t", req.Name, req.Required)))
|
||||
}
|
||||
return out
|
||||
return okFinding("audio", fmt.Sprintf("%d remote .flac object(s)", len(check.Keys)))
|
||||
}
|
||||
|
||||
func loadLocalManifest(ctx context.Context, path string) (*manifest.Manifest, error) {
|
||||
|
||||
@@ -884,6 +884,31 @@ func TestExecuteStatusReportsMissingRemoteCurrentStateWithoutFailing(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteStatusReportsPreviousStateReadinessWithoutFailing(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
||||
replaceInFileOrFatal(t, pipelinePath, "source: narratio.artifact.session_recap", "source: narratio.previous_session.artifact.session_recap")
|
||||
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})
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
"session", "status", "2026-05-03",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Previous-session artifacts: unavailable: remote current run pointer missing") {
|
||||
t.Fatalf("stdout = %q, want previous readiness unavailable line", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionValidateReportsPreviousStateFindingAndReturnsFindingError(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
||||
|
||||
274
internal/app/operator_inspection.go
Normal file
274
internal/app/operator_inspection.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
)
|
||||
|
||||
type stableInputCheck struct {
|
||||
Name string
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
type localAudioCheck struct {
|
||||
Checked bool
|
||||
Paths []string
|
||||
Err error
|
||||
}
|
||||
|
||||
type remoteAudioCheck struct {
|
||||
Checked bool
|
||||
Prefix string
|
||||
Keys []string
|
||||
Err error
|
||||
}
|
||||
|
||||
type previousArtifactReadiness struct {
|
||||
Requirements []artifacts.PreviousArtifactRequirement
|
||||
MissingID bool
|
||||
Err error
|
||||
}
|
||||
|
||||
type remoteCurrentStateCheck struct {
|
||||
State *RemoteCurrentState
|
||||
Err error
|
||||
}
|
||||
|
||||
type effectiveLocksCheck struct {
|
||||
Locks *effectiveLocks
|
||||
Err error
|
||||
}
|
||||
|
||||
func inspectStableInputs(cfg *config.Config) []stableInputCheck {
|
||||
items := []struct {
|
||||
name string
|
||||
in config.ResolvedInputFile
|
||||
}{
|
||||
{name: "speakers", in: cfg.StableInputs.SpeakersFile},
|
||||
{name: "autocorrect", in: cfg.StableInputs.AutocorrectFile},
|
||||
{name: "glossary", in: cfg.StableInputs.GlossaryFile},
|
||||
}
|
||||
out := make([]stableInputCheck, 0, len(items))
|
||||
for _, item := range items {
|
||||
path, err := resolveHelperConfigRelativePath(item.in)
|
||||
if err != nil {
|
||||
out = append(out, stableInputCheck{Name: item.name, Err: err})
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
out = append(out, stableInputCheck{Name: item.name, Path: path, Err: err})
|
||||
continue
|
||||
}
|
||||
out = append(out, stableInputCheck{Name: item.name, Path: path})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func inspectLocalAudioPresence(cfg *config.Config) localAudioCheck {
|
||||
if cfg.Session.Inputs.AudioS3 != nil {
|
||||
return localAudioCheck{}
|
||||
}
|
||||
|
||||
sessionDir := filepath.Dir(cfg.SessionPath)
|
||||
resolved, err := resolveLocalInspectionAudioPaths(sessionDir, cfg.Session.Inputs)
|
||||
if err != nil {
|
||||
return localAudioCheck{Checked: true, Err: err}
|
||||
}
|
||||
return localAudioCheck{
|
||||
Checked: true,
|
||||
Paths: resolved,
|
||||
}
|
||||
}
|
||||
|
||||
func inspectRemoteAudioPresence(ctx context.Context, cfg *config.Config, store storage.ObjectStore) remoteAudioCheck {
|
||||
if cfg.Session.Inputs.AudioS3 == nil {
|
||||
return remoteAudioCheck{}
|
||||
}
|
||||
if store == nil {
|
||||
return remoteAudioCheck{Checked: true, Err: fmt.Errorf("storage backend is required for remote audio checks")}
|
||||
}
|
||||
|
||||
sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
audioPrefix := artifacts.S3AudioPrefix(sessionPrefix, cfg.Session.Inputs.AudioS3.Prefix)
|
||||
objects, err := store.List(ctx, audioPrefix)
|
||||
if err != nil {
|
||||
return remoteAudioCheck{Checked: true, Prefix: audioPrefix, Err: err}
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(objects))
|
||||
seenBase := map[string]string{}
|
||||
for _, obj := range objects {
|
||||
key := strings.TrimSpace(obj.Key)
|
||||
if key == "" || strings.HasSuffix(key, "/") || !isInspectionFlacPath(key) {
|
||||
continue
|
||||
}
|
||||
base := path.Base(key)
|
||||
if prev, exists := seenBase[base]; exists && prev != key {
|
||||
return remoteAudioCheck{
|
||||
Checked: true,
|
||||
Prefix: audioPrefix,
|
||||
Err: fmt.Errorf("duplicate s3 audio basename %q from %q and %q", base, prev, key),
|
||||
}
|
||||
}
|
||||
seenBase[base] = key
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
if len(keys) == 0 {
|
||||
return remoteAudioCheck{
|
||||
Checked: true,
|
||||
Prefix: audioPrefix,
|
||||
Err: fmt.Errorf("no .flac files found under s3 audio prefix %q", audioPrefix),
|
||||
}
|
||||
}
|
||||
return remoteAudioCheck{
|
||||
Checked: true,
|
||||
Prefix: audioPrefix,
|
||||
Keys: keys,
|
||||
}
|
||||
}
|
||||
|
||||
func inspectPreviousArtifactReadiness(
|
||||
ctx context.Context,
|
||||
cfg *config.Config,
|
||||
store storage.ObjectStore,
|
||||
requirements []artifacts.PreviousArtifactRequirement,
|
||||
) previousArtifactReadiness {
|
||||
out := previousArtifactReadiness{
|
||||
Requirements: append([]artifacts.PreviousArtifactRequirement(nil), requirements...),
|
||||
}
|
||||
if len(requirements) == 0 {
|
||||
return out
|
||||
}
|
||||
if strings.TrimSpace(cfg.Session.PreviousSessionID) == "" {
|
||||
out.MissingID = true
|
||||
return out
|
||||
}
|
||||
if store == nil {
|
||||
out.Err = fmt.Errorf("previous-session artifacts cannot be checked because storage is unavailable")
|
||||
return out
|
||||
}
|
||||
|
||||
prefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID)
|
||||
if _, err := artifacts.LoadCurrentState(ctx, store, prefix, artifacts.CurrentStateValidation{
|
||||
ExpectedSessionID: strings.TrimSpace(cfg.Session.PreviousSessionID),
|
||||
ExpectedCampaign: strings.TrimSpace(cfg.Session.Campaign),
|
||||
ValidateRunID: true,
|
||||
}); err != nil {
|
||||
out.Err = fmt.Errorf("remote %v", err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func inspectRemoteCurrentState(ctx context.Context, cfg *config.Config, store storage.ObjectStore) remoteCurrentStateCheck {
|
||||
if store == nil {
|
||||
return remoteCurrentStateCheck{}
|
||||
}
|
||||
current, err := discoverRemoteCurrentStateFn(ctx, cfg, store)
|
||||
if err != nil {
|
||||
return remoteCurrentStateCheck{Err: err}
|
||||
}
|
||||
return remoteCurrentStateCheck{State: current}
|
||||
}
|
||||
|
||||
func inspectEffectiveLocks(ctx context.Context, cfg *config.Config, store storage.ObjectStore) effectiveLocksCheck {
|
||||
locks, err := loadEffectiveLocks(ctx, cfg, store)
|
||||
if err != nil {
|
||||
return effectiveLocksCheck{Err: err}
|
||||
}
|
||||
return effectiveLocksCheck{Locks: locks}
|
||||
}
|
||||
|
||||
func resolveLocalInspectionAudioPaths(sessionDir string, inputs config.SessionInputsConfig) ([]string, error) {
|
||||
if len(inputs.AudioFiles) > 0 {
|
||||
out := make([]string, 0, len(inputs.AudioFiles))
|
||||
seenBase := map[string]string{}
|
||||
for _, item := range inputs.AudioFiles {
|
||||
resolved, err := resolveInspectionPath(sessionDir, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isInspectionFlacPath(resolved) {
|
||||
return nil, fmt.Errorf("audio file %q must have .flac extension", resolved)
|
||||
}
|
||||
if err := requireInspectionFile(resolved, "audio file"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base := filepath.Base(resolved)
|
||||
if prev, exists := seenBase[base]; exists && prev != resolved {
|
||||
return nil, fmt.Errorf("duplicate audio basename %q from %q and %q", base, prev, resolved)
|
||||
}
|
||||
seenBase[base] = resolved
|
||||
out = append(out, resolved)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
audioDir, err := resolveInspectionPath(sessionDir, inputs.AudioDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(audioDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read audio directory %q: %w", audioDir, err)
|
||||
}
|
||||
out := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
full := filepath.Join(audioDir, entry.Name())
|
||||
if !isInspectionFlacPath(full) {
|
||||
continue
|
||||
}
|
||||
if err := requireInspectionFile(full, "audio file"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, full)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("no .flac files found in audio directory %q", audioDir)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func resolveInspectionPath(baseDir, inputPath string) (string, error) {
|
||||
pathValue := strings.TrimSpace(inputPath)
|
||||
if pathValue == "" {
|
||||
return "", fmt.Errorf("path is required")
|
||||
}
|
||||
if filepath.IsAbs(pathValue) {
|
||||
return filepath.Clean(pathValue), nil
|
||||
}
|
||||
return filepath.Clean(filepath.Join(baseDir, pathValue)), nil
|
||||
}
|
||||
|
||||
func requireInspectionFile(path, label string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("%s %q does not exist", label, path)
|
||||
}
|
||||
return fmt.Errorf("stat %s %q: %w", label, path, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return fmt.Errorf("%s %q is a directory", label, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isInspectionFlacPath(path string) bool {
|
||||
return strings.EqualFold(filepath.Ext(strings.TrimSpace(path)), ".flac")
|
||||
}
|
||||
@@ -54,23 +54,26 @@ func SessionValidate(ctx context.Context, args []string, out io.Writer) error {
|
||||
}
|
||||
|
||||
requirements := artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg))
|
||||
if len(requirements) == 0 {
|
||||
previous := inspectPreviousArtifactReadiness(ctx, cfg, store, requirements)
|
||||
if len(previous.Requirements) == 0 {
|
||||
findings = append(findings, okFinding("previous", "no previous-session artifacts required"))
|
||||
} else if strings.TrimSpace(cfg.Session.PreviousSessionID) == "" {
|
||||
} else if previous.MissingID {
|
||||
findings = append(findings, errorFinding("previous", "previous_session_id is required by configured previous-session artifacts"))
|
||||
} else if storeErr != nil {
|
||||
findings = append(findings, errorFinding("previous", "previous-session artifacts cannot be checked because storage is unavailable"))
|
||||
} else if previous.Err != nil {
|
||||
findings = append(findings, errorFinding("previous", previous.Err.Error()))
|
||||
} else {
|
||||
findings = append(findings, validatePreviousArtifactFindings(ctx, cfg, store, requirements)...)
|
||||
for _, req := range previous.Requirements {
|
||||
findings = append(findings, okFinding("previous", fmt.Sprintf("%s required=%t", req.Name, req.Required)))
|
||||
}
|
||||
}
|
||||
|
||||
locks, lockErr := loadEffectiveLocks(ctx, cfg, store)
|
||||
if lockErr != nil {
|
||||
findings = append(findings, errorFinding("locks", lockErr.Error()))
|
||||
} else if len(locks.All) == 0 {
|
||||
locks := inspectEffectiveLocks(ctx, cfg, store)
|
||||
if locks.Err != nil {
|
||||
findings = append(findings, errorFinding("locks", locks.Err.Error()))
|
||||
} else if len(locks.Locks.All) == 0 {
|
||||
findings = append(findings, okFinding("locks", "no effective publish locks"))
|
||||
} else {
|
||||
for _, lock := range locks.All {
|
||||
for _, lock := range locks.Locks.All {
|
||||
findings = append(findings, warnFinding("locks", fmt.Sprintf("%s locked: %s", lock.Source, strings.TrimSpace(lock.Reason))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
)
|
||||
@@ -35,6 +37,8 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
|
||||
fmt.Fprintf(out, "Campaign: %s\n", cfg.Session.Campaign)
|
||||
fmt.Fprintf(out, "Workspace: %s\n", paths.Root)
|
||||
fmt.Fprintf(out, "Session config: %s\n", sessionSourceSummary(cfg))
|
||||
writeStatusStableInputs(out, inspectStableInputs(cfg))
|
||||
writeStatusLocalAudio(out, inspectLocalAudioPresence(cfg))
|
||||
|
||||
if m, err := loadLocalManifest(ctx, paths.ManifestPath); err != nil {
|
||||
fmt.Fprintf(out, "Local manifest: error: %v\n", err)
|
||||
@@ -49,21 +53,30 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
|
||||
if storeErr != nil {
|
||||
fmt.Fprintf(out, "Remote publish: unavailable: %v\n", storeErr)
|
||||
} else if store != nil {
|
||||
current, err := discoverRemoteCurrentStateFn(ctx, cfg, store)
|
||||
if err != nil {
|
||||
fmt.Fprintf(out, "Remote publish: missing or unavailable: %v\n", err)
|
||||
current := inspectRemoteCurrentState(ctx, cfg, store)
|
||||
if current.Err != nil {
|
||||
fmt.Fprintf(out, "Remote publish: missing or unavailable: %v\n", current.Err)
|
||||
} else {
|
||||
fmt.Fprintf(out, "Remote publish: current run %s\n", current.RunID)
|
||||
fmt.Fprintf(out, "Remote manifest: %s\n", current.CurrentManifestKey)
|
||||
fmt.Fprintf(out, "Remote publish: current run %s\n", current.State.RunID)
|
||||
fmt.Fprintf(out, "Remote manifest: %s\n", current.State.CurrentManifestKey)
|
||||
}
|
||||
}
|
||||
writeStatusRemoteAudio(ctx, out, cfg, store, storeErr)
|
||||
writeStatusPreviousArtifacts(out, inspectPreviousArtifactReadiness(
|
||||
ctx,
|
||||
cfg,
|
||||
store,
|
||||
artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg)),
|
||||
))
|
||||
|
||||
locks, err := loadEffectiveLocks(ctx, cfg, store)
|
||||
lockChecks := inspectEffectiveLocks(ctx, cfg, store)
|
||||
locks := lockChecks.Locks
|
||||
lockErr := lockChecks.Err
|
||||
if catalog, catalogErr := buildHelperArtifactCatalog(cfg); catalogErr != nil {
|
||||
fmt.Fprintf(out, "Remote outputs: error: %v\n", catalogErr)
|
||||
} else if storeErr == nil {
|
||||
catalogLocks := locks
|
||||
if err != nil {
|
||||
if lockErr != nil {
|
||||
catalogLocks = &effectiveLocks{
|
||||
Static: staticPublishLocks(cfg),
|
||||
All: staticPublishLocks(cfg),
|
||||
@@ -76,8 +89,8 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
|
||||
fmt.Fprintln(out, "Remote outputs:")
|
||||
writeArtifactList(out, cfg, catalog, catalogLocks, publishedRemoteState)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(out, "Publish locks: error: %v\n", err)
|
||||
if lockErr != nil {
|
||||
fmt.Fprintf(out, "Publish locks: error: %v\n", lockErr)
|
||||
} else {
|
||||
writeLocks(out, cfg, locks)
|
||||
}
|
||||
@@ -86,3 +99,68 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
|
||||
fmt.Fprintf(out, "- narratio session restore %s --dry-run\n", cfg.Session.SessionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeStatusStableInputs(out io.Writer, checks []stableInputCheck) {
|
||||
if len(checks) == 0 {
|
||||
return
|
||||
}
|
||||
for _, check := range checks {
|
||||
if check.Err != nil {
|
||||
if strings.TrimSpace(check.Path) != "" {
|
||||
fmt.Fprintf(out, "Stable input %s: unavailable: %v\n", check.Name, check.Err)
|
||||
} else {
|
||||
fmt.Fprintf(out, "Stable input %s: unavailable: %s\n", check.Name, check.Err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(out, "Stable input %s: %s\n", check.Name, check.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func writeStatusLocalAudio(out io.Writer, check localAudioCheck) {
|
||||
if !check.Checked {
|
||||
return
|
||||
}
|
||||
if check.Err != nil {
|
||||
fmt.Fprintf(out, "Local audio: unavailable: %v\n", check.Err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(out, "Local audio: %d file(s)\n", len(check.Paths))
|
||||
}
|
||||
|
||||
func writeStatusRemoteAudio(ctx context.Context, out io.Writer, cfg *config.Config, store storage.ObjectStore, storeErr error) {
|
||||
if cfg.Session.Inputs.AudioS3 == nil {
|
||||
return
|
||||
}
|
||||
if storeErr != nil {
|
||||
fmt.Fprintf(out, "Remote audio: unavailable: %v\n", storeErr)
|
||||
return
|
||||
}
|
||||
check := inspectRemoteAudioPresence(ctx, cfg, store)
|
||||
if check.Err != nil {
|
||||
fmt.Fprintf(out, "Remote audio: unavailable: %v\n", check.Err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(out, "Remote audio: %d .flac object(s)\n", len(check.Keys))
|
||||
}
|
||||
|
||||
func writeStatusPreviousArtifacts(out io.Writer, readiness previousArtifactReadiness) {
|
||||
if len(readiness.Requirements) == 0 {
|
||||
fmt.Fprintln(out, "Previous-session artifacts: not required")
|
||||
return
|
||||
}
|
||||
if readiness.MissingID {
|
||||
fmt.Fprintln(out, "Previous-session artifacts: unavailable: previous_session_id is required by configured previous-session artifacts")
|
||||
return
|
||||
}
|
||||
if readiness.Err != nil {
|
||||
fmt.Fprintf(out, "Previous-session artifacts: unavailable: %v\n", readiness.Err)
|
||||
return
|
||||
}
|
||||
names := make([]string, 0, len(readiness.Requirements))
|
||||
for _, req := range readiness.Requirements {
|
||||
names = append(names, fmt.Sprintf("%s(required=%t)", req.Name, req.Required))
|
||||
}
|
||||
sort.Strings(names)
|
||||
fmt.Fprintf(out, "Previous-session artifacts: ready: %s\n", strings.Join(names, ", "))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user