From 3752f3ed2801d0714178ab4ceca0d40d33768b4b Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 21 May 2026 20:49:28 -0500 Subject: [PATCH] Added remote artifact listing to narratio status --- docs/cli.md | 1 + docs/operations.md | 3 ++ internal/app/operator_helpers.go | 17 ++++++ internal/app/operator_helpers_test.go | 74 +++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/docs/cli.md b/docs/cli.md index 6d23b17..467a78a 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -231,6 +231,7 @@ Session output includes: - session ID, campaign, workspace, session config source. - local manifest state when present. - remote current archive state when storage is configured. +- catalog-based remote output availability for expected transcript and artifact sources. - effective archive locks and conservative next actions. Common failure cases: diff --git a/docs/operations.md b/docs/operations.md index 2bbff1e..ef4e349 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -219,6 +219,8 @@ Recommended recovery: narratio status --session-id 2026-04-04 ``` +This reports local manifest state, committed remote current state, expected remote transcript/artifact availability, and archive locks. + 2. for one manifest file, run: ```bash @@ -251,6 +253,7 @@ Dry-run does not write restore report files. - `status` with no config/session flags still requires explicit `--manifest`. - `status --session-id ` uses normal config/session loading, including remote session fallback. +- `status --session-id ` includes the same source-based remote output availability view as `artifacts list --remote` when storage is configured. - local and S3 audio input modes are mutually exclusive. - archive publish requires upstream stages through `analyze` to be `succeeded`. - required promotion rules can fail when selected analyze artifacts did not generate a required file path. diff --git a/internal/app/operator_helpers.go b/internal/app/operator_helpers.go index 14aa8a6..bb0a46c 100644 --- a/internal/app/operator_helpers.go +++ b/internal/app/operator_helpers.go @@ -208,6 +208,23 @@ func Status(ctx context.Context, args []string, out io.Writer) error { } locks, err := loadEffectiveLocks(ctx, cfg, store) + 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 { + catalogLocks = &effectiveLocks{ + Static: staticArchiveLocks(cfg), + All: staticArchiveLocks(cfg), + } + } + remoteState := map[string]string{} + if store != nil { + remoteState = remoteArtifactAvailability(ctx, cfg, store, catalog) + } + fmt.Fprintln(out, "Remote outputs:") + writeArtifactList(out, cfg, catalog, catalogLocks, remoteState) + } if err != nil { fmt.Fprintf(out, "Archive locks: error: %v\n", err) } else { diff --git a/internal/app/operator_helpers_test.go b/internal/app/operator_helpers_test.go index 22b7c54..467e127 100644 --- a/internal/app/operator_helpers_test.go +++ b/internal/app/operator_helpers_test.go @@ -337,6 +337,80 @@ func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) { } } +func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) { + workspaceRoot := t.TempDir() + pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) + fake := &storage.FakeBackend{} + sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03") + manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) + trimmedKey := artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/trimmed.json") + lockKey := artifacts.S3SessionLocksKey(sessionPrefix) + 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")}) + fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte(`{"segments":[]}`)}) + fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.trimmed\n reason: remote review\n")}) + var storeInitCalls int + restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) + + var stdout bytes.Buffer + var stderr bytes.Buffer + code := Execute([]string{ + "status", + "--config", pipelinePath, + "--campaign", campaignPath, + "--session", sessionPath, + "--session-id", "2026-05-03", + }, &stdout, &stderr) + if code != 0 { + t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) + } + out := stdout.String() + for _, want := range []string{ + "Remote outputs:", + "Built-in:", + "Configured:", + "Previous-session:", + "Promoted:", + "narratio.transcript.trimmed locked remote=promoted", + "narratio.transcript.merged remote=missing", + } { + if !strings.Contains(out, want) { + t.Fatalf("stdout = %q, want %q", out, want) + } + } +} + +func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testing.T) { + workspaceRoot := t.TempDir() + pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) + fake := &storage.FakeBackend{ExistsErr: fmt.Errorf("exists failed")} + var storeInitCalls int + restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) + + var stdout bytes.Buffer + var stderr bytes.Buffer + code := Execute([]string{ + "status", + "--config", pipelinePath, + "--campaign", campaignPath, + "--session", sessionPath, + "--session-id", "2026-05-03", + }, &stdout, &stderr) + if code != 0 { + t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) + } + out := stdout.String() + if !strings.Contains(out, "Remote archive: missing or unavailable:") { + t.Fatalf("stdout = %q, want remote archive unavailable state", out) + } + if !strings.Contains(out, "Remote outputs:") || !strings.Contains(out, "narratio.transcript.trimmed remote=error") { + t.Fatalf("stdout = %q, want remote output error state", out) + } + if !strings.Contains(out, "Archive locks: error:") { + t.Fatalf("stdout = %q, want archive locks error", out) + } +} + func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidArchiveConfigFiles(t, workspaceRoot)