package app import ( "bytes" "context" "fmt" "os" "path/filepath" "sort" "strings" "testing" "time" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/artifactmodel" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) func TestExecuteSessionInitRemoteWritesCanonicalSessionConfig(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "init", "2026-06-07", "--config", pipelinePath, "--campaign-file", campaignPath, "--title", "The Black Cabin", "--remote", }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } key := artifacts.S3SessionConfigKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-06-07")) obj, ok := fake.Objects[key] if !ok { t.Fatalf("remote session key %q not uploaded; objects=%v", key, fake.Objects) } if !strings.Contains(string(obj.Data), `session_id: "2026-06-07"`) || !strings.Contains(string(obj.Data), "prefix: audio/") { t.Fatalf("remote session data = %q", string(obj.Data)) } if storeInitCalls != 1 { t.Fatalf("object store init calls = %d, want 1", storeInitCalls) } } func TestExecuteSessionInitRemoteUsesDefaultConfigDiscovery(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) withDefaultPipelineCampaignConfigs(t, pipelinePath, campaignPath) fake := &storage.FakeBackend{} var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer 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()) } key := artifacts.S3SessionConfigKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-06-07")) if _, ok := fake.Objects[key]; !ok { t.Fatalf("remote session key %q not uploaded; objects=%v", key, fake.Objects) } if storeInitCalls != 1 { t.Fatalf("object store init calls = %d, want 1", storeInitCalls) } } func TestExecuteSessionInitLocalUsesDefaultConfigDiscovery(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) withDefaultPipelineCampaignConfigs(t, pipelinePath, campaignPath) outputPath := filepath.Join(t.TempDir(), "session.yml") var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{ "session", "init", "2026-06-07", "--output", outputPath, }, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } data, err := os.ReadFile(outputPath) if err != nil { t.Fatalf("read generated session: %v", err) } if !strings.Contains(string(data), `session_id: "2026-06-07"`) || !strings.Contains(string(data), "prefix: audio/") { t.Fatalf("generated session = %q", string(data)) } } func TestExecuteSessionInitExplicitConfigWinsOverDefaults(t *testing.T) { workspaceRoot := t.TempDir() defaultPipeline, defaultCampaign, _ := writeValidConfigFiles(t, workspaceRoot) withDefaultPipelineCampaignConfigs(t, defaultPipeline, defaultCampaign) explicitDir := t.TempDir() explicitCampaign := filepath.Join(explicitDir, "campaign.yml") if err := os.WriteFile(explicitCampaign, []byte(`campaign_id: explicit-campaign inputs: speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml party_file: ./party.yml `), 0o644); err != nil { t.Fatalf("write explicit campaign: %v", err) } mustWriteTestFile(t, filepath.Join(explicitDir, "speakers.yml"), "match:\n - speaker: Alice\n match: [\"alice\"]\n") mustWriteTestFile(t, filepath.Join(explicitDir, "autocorrect.yml"), "[]\n") mustWriteTestFile(t, filepath.Join(explicitDir, "glossary.yml"), "[]\n") mustWriteTestFile(t, filepath.Join(explicitDir, "players.yml"), "[]\n") mustWriteTestFile(t, filepath.Join(explicitDir, "party.yml"), "schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n") fake := &storage.FakeBackend{} var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "init", "2026-06-07", "--config", defaultPipeline, "--campaign-file", explicitCampaign, "--remote", }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } explicitKey := artifacts.S3SessionConfigKey(artifacts.S3SessionPrefix("dnd", "explicit-campaign", "2026-06-07")) if _, ok := fake.Objects[explicitKey]; !ok { t.Fatalf("explicit campaign remote key %q not uploaded; objects=%v", explicitKey, fake.Objects) } defaultKey := artifacts.S3SessionConfigKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-06-07")) if _, ok := fake.Objects[defaultKey]; ok { t.Fatalf("default campaign key %q uploaded despite explicit campaign override", defaultKey) } } func TestExecuteSessionInitRequiresSessionID(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) withDefaultPipelineCampaignConfigs(t, pipelinePath, campaignPath) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "init", "--remote"}, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "session init: session_id is required") { t.Fatalf("stderr = %q, want session-id required error", stderr.String()) } } func TestExecuteSessionInitMissingDefaultConfigReportsSearchedPaths(t *testing.T) { origPipelineDefaults := append([]string(nil), config.DefaultPipelineConfigSearchPaths...) config.DefaultPipelineConfigSearchPaths = []string{filepath.Join(t.TempDir(), "missing-pipeline.yml")} t.Cleanup(func() { config.DefaultPipelineConfigSearchPaths = origPipelineDefaults }) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "init", "2026-06-07", "--remote"}, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "session init: no pipeline config path provided and no default pipeline config found; searched:") { t.Fatalf("stderr = %q, want default pipeline searched-path error", stderr.String()) } } func TestExecuteSessionInitRemoteLoadsSecretsBeforeObjectStoreInit(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) withDefaultPipelineCampaignConfigs(t, pipelinePath, campaignPath) accessKeyEnv := "NARRATIO_TEST_SESSION_INIT_OBJECT_KEY_ID" secretKeyEnv := "NARRATIO_TEST_SESSION_INIT_OBJECT_SECRET" restoreEnvAfterTest(t, accessKeyEnv, secretKeyEnv) secretsDir := t.TempDir() mustWriteSecretFile(t, filepath.Join(secretsDir, accessKeyEnv), "test-key-id\n") mustWriteSecretFile(t, filepath.Join(secretsDir, secretKeyEnv), "test-secret\n") addSecretsToPipelineConfig(t, pipelinePath, secretsDir, accessKeyEnv, secretKeyEnv) fake := &storage.FakeBackend{} 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 }} var stdout bytes.Buffer var stderr bytes.Buffer 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()) } } func TestExecuteSessionInitLocalRendersCampaignTemplate(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) writeSessionInitTemplate(t, campaignPath, `session_id: "{{ session_id }}" previous_session_id: "{{ previous_session_id }}" date: "{{ date }}" title: "{{ title }}" inputs: audio_s3: prefix: "{{ audio_s3_prefix }}" `) outputPath := filepath.Join(t.TempDir(), "session.yml") var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{ "session", "init", "2026-06-07", "--config", pipelinePath, "--campaign-file", campaignPath, "--previous-session-id", "2026-05-31", "--date", "2026-06-07", "--title", "The Black Cabin", "--audio-s3-prefix", "audio/", "--output", outputPath, }, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } data, err := os.ReadFile(outputPath) if err != nil { t.Fatalf("read generated session: %v", err) } got := string(data) for _, want := range []string{ `session_id: "2026-06-07"`, `previous_session_id: "2026-05-31"`, `date: "2026-06-07"`, `title: "The Black Cabin"`, `prefix: "audio/"`, } { if !strings.Contains(got, want) { t.Fatalf("generated session = %q, want %q", got, want) } } if strings.Contains(got, "{{") { t.Fatalf("generated session still contains template placeholder: %q", got) } } func TestExecuteSessionInitRemoteRendersCampaignTemplate(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) writeSessionInitTemplate(t, campaignPath, `session_id: "{{ session_id }}" inputs: audio_s3: prefix: audio/ `) fake := &storage.FakeBackend{} var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "init", "2026-06-07", "--config", pipelinePath, "--campaign-file", campaignPath, "--remote", }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } key := artifacts.S3SessionConfigKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-06-07")) obj, ok := fake.Objects[key] if !ok { t.Fatalf("remote session key %q not uploaded; objects=%v", key, fake.Objects) } if strings.Contains(string(obj.Data), "{{") || !strings.Contains(string(obj.Data), `session_id: "2026-06-07"`) { t.Fatalf("remote session data = %q, want rendered concrete session", string(obj.Data)) } } func TestExecuteSessionInitTemplatePathIsCampaignRelative(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) templateDir := filepath.Join(filepath.Dir(campaignPath), "templates") if err := os.MkdirAll(templateDir, 0o755); err != nil { t.Fatalf("mkdir template dir: %v", err) } templatePath := filepath.Join(templateDir, "session.template.yml") if err := os.WriteFile(templatePath, []byte(`session_id: "{{ session_id }}" inputs: audio_dir: ./audio `), 0o644); err != nil { t.Fatalf("write session template: %v", err) } addSessionTemplateToCampaign(t, campaignPath, "./templates/session.template.yml") outputPath := filepath.Join(t.TempDir(), "session.yml") var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{ "session", "init", "2026-06-07", "--config", pipelinePath, "--campaign-file", campaignPath, "--output", outputPath, }, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } data, err := os.ReadFile(outputPath) if err != nil { t.Fatalf("read generated session: %v", err) } if !strings.Contains(string(data), `session_id: "2026-06-07"`) { t.Fatalf("generated session = %q, want campaign-relative template output", string(data)) } } func TestExecuteSessionInitTemplateMissingVariableFails(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) writeSessionInitTemplate(t, campaignPath, `session_id: "{{ session_id }}" date: "{{ date }}" inputs: audio_s3: prefix: audio/ `) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{ "session", "init", "2026-06-07", "--config", pipelinePath, "--campaign-file", campaignPath, "--remote", }, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "missing required template variable value(s): date") { t.Fatalf("stderr = %q, want missing date variable", stderr.String()) } } func TestExecuteSessionInitTemplateUnusedFlagFails(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) writeSessionInitTemplate(t, campaignPath, `session_id: "{{ session_id }}" inputs: audio_s3: prefix: audio/ `) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{ "session", "init", "2026-06-07", "--config", pipelinePath, "--campaign-file", campaignPath, "--title", "Unused Title", "--remote", }, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "unused template variable value(s): title") { t.Fatalf("stderr = %q, want unused title variable", stderr.String()) } } func TestExecuteSessionInitTemplateStrictDecodeFailure(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) writeSessionInitTemplate(t, campaignPath, `session_id: "{{ session_id }}" unknown: true inputs: audio_s3: prefix: audio/ `) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{ "session", "init", "2026-06-07", "--config", pipelinePath, "--campaign-file", campaignPath, "--remote", }, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "strict decode failed") { t.Fatalf("stderr = %q, want strict decode error", stderr.String()) } } func TestExecuteSessionValidateLoadsSecretsBeforeObjectStoreInit(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) accessKeyEnv := "NARRATIO_TEST_VALIDATE_OBJECT_KEY_ID" secretKeyEnv := "NARRATIO_TEST_VALIDATE_OBJECT_SECRET" restoreEnvAfterTest(t, accessKeyEnv, secretKeyEnv) secretsDir := t.TempDir() mustWriteSecretFile(t, filepath.Join(secretsDir, accessKeyEnv), "test-key-id\n") mustWriteSecretFile(t, filepath.Join(secretsDir, secretKeyEnv), "test-secret\n") addSecretsToPipelineConfig(t, pipelinePath, secretsDir, accessKeyEnv, secretKeyEnv) if err := os.WriteFile(sessionPath, []byte(`session_id: 2026-05-03 inputs: audio_s3: prefix: audio/ `), 0o644); err != nil { t.Fatalf("write session: %v", err) } 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")}) 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 }} var stdout bytes.Buffer var stderr bytes.Buffer 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()) } if !strings.Contains(stdout.String(), "OK audio") { t.Fatalf("stdout = %q, want OK audio", stdout.String()) } if !strings.Contains(stdout.String(), "OK inputs players:") || !strings.Contains(stdout.String(), "OK inputs party:") { t.Fatalf("stdout = %q, want players and party input findings", stdout.String()) } } func TestOperatorCommandsReportConfiguredSpellCatalog(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) campaignBytes, err := os.ReadFile(campaignPath) if err != nil { t.Fatalf("read campaign: %v", err) } campaignYAML := strings.Replace(string(campaignBytes), " party_file: ./party.yml\n", " party_file: ./party.yml\n spell_catalog_file: ./spells.json\n", 1) if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil { t.Fatalf("write campaign: %v", err) } spellPath := filepath.Join(filepath.Dir(campaignPath), "spells.json") mustWriteTestFile(t, spellPath, "{\"spells\":[]}\n") fake := &storage.FakeBackend{} var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) commonArgs := []string{ "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, } var stdout bytes.Buffer var stderr bytes.Buffer validateArgs := append([]string{"session", "validate"}, commonArgs...) 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) { t.Fatalf("validate stdout = %q, want spell catalog finding", stdout.String()) } stdout.Reset() stderr.Reset() statusArgs := append([]string{"session", "status"}, commonArgs...) 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) { t.Fatalf("status stdout = %q, want spell catalog inventory", stdout.String()) } if err := os.Remove(spellPath); err != nil { t.Fatalf("remove spell catalog: %v", err) } stdout.Reset() stderr.Reset() 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:") { t.Fatalf("validate stdout = %q, want missing spell catalog finding", stdout.String()) } } func TestExecuteLocksAddListAndRemoveUseIndependentObjects(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer 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, deps, executePlan) if code != 0 { t.Fatalf("locks add exit code = %d, want 0; stderr=%q", code, stderr.String()) } key, err := artifacts.RemoteLockKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "narratio.transcript.final_trimmed") if err != nil { t.Fatal(err) } obj, ok := fake.Objects[key] if !ok { t.Fatalf("remote locks key %q not uploaded", key) } lock, err := artifacts.DecodeRemoteLock(obj.Data) if err != nil || lock.Source != "narratio.transcript.final_trimmed" || lock.Reason != "manual edit" { t.Fatalf("lock object = %#v, %v", lock, err) } stdout.Reset() stderr.Reset() code = executeWithDependencies([]string{ "session", "locks", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("locks list exit code = %d, want 0; stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), "- narratio.transcript.final_trimmed published reason=manual edit created_at=") { t.Fatalf("stdout = %q, want remote lock", stdout.String()) } stdout.Reset() stderr.Reset() code = executeWithDependencies([]string{ "session", "locks", "remove", "2026-05-03", "narratio.transcript.final_trimmed", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String()) } if _, exists := fake.Objects[key]; exists { t.Fatalf("lock object %q remains after remove", key) } stdout.Reset() stderr.Reset() code = executeWithDependencies([]string{ "session", "locks", "remove", "2026-05-03", "narratio.transcript.final_trimmed", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &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()) } if storeInitCalls != 4 { t.Fatalf("object store init calls = %d, want 4", storeInitCalls) } } func TestExecuteLocksListIsSortedAndRejectsMalformedObjects(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} var storeInitCalls int 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") args := []string{"session", "locks", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath} var stdout bytes.Buffer var stderr bytes.Buffer 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 ") second := strings.Index(stdout.String(), "narratio.transcript.final_trimmed ") if first < 0 || second < 0 || first >= second { t.Fatalf("locks list is not source sorted: %q", stdout.String()) } badKey, err := artifacts.RemoteLockKey(sessionPrefix, "narratio.transcript.base") if err != nil { t.Fatal(err) } fake.SeedObject(storage.FakeObject{Key: badKey, Data: []byte(`{"format_version":1}`)}) stdout.Reset() stderr.Reset() if code := executeWithDependencies(args, &stdout, &stderr, deps, executePlan); code == 0 { t.Fatalf("malformed lock list exit code = 0; stdout=%q", stdout.String()) } } func TestExecuteLocksAddDuplicateRequiresForce(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--reason", "first", }, &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 = executeWithDependencies([]string{ "session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--reason", "second", }, &stdout, &stderr, deps, executePlan) if code == 0 { t.Fatal("duplicate locks add exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "pass --force to update") { t.Fatalf("stderr = %q, want force guidance", stderr.String()) } stdout.Reset() stderr.Reset() 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, deps, executePlan) if code != 0 { t.Fatalf("forced locks add exit code = %d, want 0; stderr=%q", code, stderr.String()) } key, err := artifacts.RemoteLockKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "narratio.transcript.final_trimmed") if err != nil { t.Fatal(err) } lock, err := artifacts.DecodeRemoteLock(fake.Objects[key].Data) if err != nil || lock.Reason != "second" { t.Fatalf("lock object = %#v, %v; want updated reason", lock, err) } } func TestExecuteLocksForceReportsConcurrentReplacement(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} var storeInitCalls int 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)) if err != nil { t.Fatal(err) } replacementData, err := artifacts.EncodeRemoteLock(replacement) if err != nil { t.Fatal(err) } fake.UploadHook = func(call storage.FakeUploadCall) error { if call.Key == key { fake.SeedObject(storage.FakeObject{Key: key, Data: replacementData}) } return nil } var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed", "--force", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &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()) } } func TestExecuteLocksRequireSessionID(t *testing.T) { tests := []struct { name string args []string want string }{ {"list", []string{"session", "locks"}, "locks: session_id is required"}, {"add", []string{"session", "locks", "add", "narratio.transcript.final_trimmed"}, "locks add: expected session_id and source id"}, {"remove", []string{"session", "locks", "remove", "narratio.transcript.final_trimmed"}, "locks remove: expected session_id and source id"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := Execute(tt.args, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), tt.want) { t.Fatalf("stderr = %q, want %q", stderr.String(), tt.want) } }) } } func TestExecuteLocksMutationRejectsSessionIDMismatch(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) tests := []struct { name string args []string }{ { name: "add mismatch", args: []string{ "session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed", "--session-id", "2026-05-04", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, }, { name: "remove mismatch", args: []string{ "session", "locks", "remove", "2026-05-03", "narratio.transcript.final_trimmed", "--session-id", "2026-05-04", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies(tt.args, &stdout, &stderr, deps, executePlan) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "does not match expected session id") { t.Fatalf("stderr = %q, want session-id mismatch guidance", stderr.String()) } }) } } func TestExecuteTopLevelLockAndUnlockAreRemoved(t *testing.T) { tests := []string{"lock", "unlock"} for _, cmd := range tests { t.Run(cmd, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{cmd, "narratio.transcript.final_trimmed"}, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), `unknown command: "`+cmd+`"`) { t.Fatalf("stderr = %q, want unknown command", stderr.String()) } }) } } func withDefaultPipelineCampaignConfigs(t *testing.T, pipelinePath, campaignPath string) { t.Helper() origPipelineDefaults := append([]string(nil), config.DefaultPipelineConfigSearchPaths...) config.DefaultPipelineConfigSearchPaths = []string{pipelinePath} t.Cleanup(func() { config.DefaultPipelineConfigSearchPaths = origPipelineDefaults }) _ = campaignPath } func writeSessionInitTemplate(t *testing.T, campaignPath, templateYAML string) { t.Helper() templatePath := filepath.Join(filepath.Dir(campaignPath), "session.template.yml") if err := os.WriteFile(templatePath, []byte(templateYAML), 0o644); err != nil { t.Fatalf("write session template: %v", err) } addSessionTemplateToCampaign(t, campaignPath, "./session.template.yml") } func addSessionTemplateToCampaign(t *testing.T, campaignPath, templateFile string) { t.Helper() data, err := os.ReadFile(campaignPath) if err != nil { t.Fatalf("read campaign config: %v", err) } if strings.Contains(string(data), "session_template_file:") { t.Fatalf("campaign config already has session_template_file: %q", string(data)) } updated := "session_template_file: " + templateFile + "\n" + string(data) if err := os.WriteFile(campaignPath, []byte(updated), 0o644); err != nil { t.Fatalf("write campaign config: %v", err) } } func seedOperatorCommittedOutputs(t *testing.T, fake *storage.FakeBackend, outputs map[string]string) { t.Helper() const runID = "20260519T010203Z-a1b2c3d4" sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03") manifestKey := artifacts.S3RunSessionManifestKey(sessionPrefix, runID) fake.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "2026-05-03", "sample-campaign")}) declarations := []artifacts.RemoteArtifact{ remoteRestoreArtifact(fake, artifacts.RemoteArtifactTypeSessionManifest, "session.manifest", manifestKey), } sources := make([]string, 0, len(outputs)) for source := range outputs { sources = append(sources, source) } sort.Strings(sources) for _, source := range sources { key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, runID), "published/"+outputs[source]) fake.SeedObject(storage.FakeObject{Key: key, Data: []byte(`{"available":true}`)}) declarations = append(declarations, remoteRestoreArtifact(fake, artifacts.RemoteArtifactTypePublishedOutput, source, key)) } commit := artifacts.RemoteCommitManifest{ FormatVersion: artifacts.RemoteCommitFormatVersion, Campaign: "sample-campaign", SessionID: "2026-05-03", RunID: runID, Artifacts: declarations, } commitData, err := artifacts.EncodeRemoteCommitManifest(commit) if err != nil { t.Fatalf("encode operator remote commit: %v", err) } commitKey := artifacts.S3RunCommitKey(sessionPrefix, runID) fake.SeedObject(storage.FakeObject{Key: commitKey, Data: commitData}) commitObject := fake.Objects[commitKey] pointerData, err := artifacts.EncodeCurrentCommitPointer(artifacts.CurrentCommitPointer{ FormatVersion: artifacts.RemoteCommitFormatVersion, Campaign: "sample-campaign", SessionID: "2026-05-03", RunID: runID, CommitKey: commitKey, CommitSHA256: restoreCommitSHA256(commitData), CommitSize: int64(len(commitData)), CommitGeneration: commitObject.ETag, }) if err != nil { t.Fatalf("encode operator current pointer: %v", err) } fake.SeedObject(storage.FakeObject{Key: artifacts.S3CurrentCommitPointerKey(sessionPrefix), Data: pointerData}) } func TestExecuteArtifactsListRemoteReportsPublishedAvailability(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) addPublishOutputsToPipeline(t, pipelinePath, ` outputs: - source: narratio.transcript.final_trimmed dest: transcripts/final.trimmed.json required: true `) fake := &storage.FakeBackend{} seedOperatorCommittedOutputs(t, fake, map[string]string{ "narratio.transcript.final_trimmed": "transcripts/final.trimmed.json", }) var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "artifacts", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--remote", }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), "narratio.transcript.final_trimmed remote=published") { t.Fatalf("stdout = %q, want published remote availability", stdout.String()) } } func TestExecuteArtifactsListReportsExtractionLifecycleWithoutPayload(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) addExtractionOutputToPipeline(t, pipelinePath) addPublishOutputsToPipeline(t, pipelinePath, ` outputs: - source: narratio.extraction.encounters dest: artifacts/encounters.json required: true `) lanePath := writeOperatorExtractionManifest(t, workspaceRoot) fake := &storage.FakeBackend{} seedOperatorCommittedOutputs(t, fake, map[string]string{ "narratio.extraction.encounters": "artifacts/encounters.json", }) var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "artifacts", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--remote", }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } out := stdout.String() for _, want := range []string{ "Extraction:", "narratio.extraction.encounters planned available provenance=manifest.current_extract_run", "narratio.extraction.encounters dest=artifacts/encounters.json remote=published", } { if !strings.Contains(out, want) { t.Fatalf("stdout = %q, want %q", out, want) } } if strings.Contains(out, "DO_NOT_PRINT") { t.Fatalf("operator output exposed extraction payload: %q", out) } if err := os.Remove(lanePath); err != nil { t.Fatal(err) } stdout.Reset() stderr.Reset() code = executeWithDependencies([]string{ "session", "artifacts", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("unavailable exit code = %d, want 0; stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), "narratio.extraction.encounters planned unavailable") { t.Fatalf("stdout = %q, want unavailable extraction state", stdout.String()) } } func TestExecuteArtifactsListRemoteUsesPublishOutputDestinations(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) addPublishOutputsToPipeline(t, pipelinePath, ` outputs: - source: narratio.transcript.final dest: transcripts/full.json required: true - source: narratio.bounds.session dest: transcripts/bounds.json required: true `) fake := &storage.FakeBackend{} seedOperatorCommittedOutputs(t, fake, map[string]string{ "narratio.transcript.final": "transcripts/full.json", "narratio.bounds.session": "transcripts/bounds.json", }) var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "artifacts", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--remote", }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } out := stdout.String() for _, unwanted := range []string{ "narratio.transcript.final remote=missing", "narratio.bounds.session remote=missing", } { if strings.Contains(out, unwanted) { t.Fatalf("stdout = %q, did not want catalog remote marker %q", out, unwanted) } } for _, want := range []string{ "narratio.transcript.final dest=transcripts/full.json remote=published", "narratio.bounds.session dest=transcripts/bounds.json remote=published", } { if !strings.Contains(out, want) { t.Fatalf("stdout = %q, want %q", out, want) } } } func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) addPublishOutputsToPipeline(t, pipelinePath, ` outputs: - source: narratio.transcript.final_trimmed dest: transcripts/final.trimmed.json required: true - source: narratio.transcript.final dest: transcripts/full.json required: true `) fake := &storage.FakeBackend{} sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03") seedOperatorCommittedOutputs(t, fake, map[string]string{ "narratio.transcript.final_trimmed": "transcripts/final.trimmed.json", "narratio.transcript.final": "transcripts/full.json", }) seedOperatorRemoteLock(t, fake, sessionPrefix, "narratio.transcript.final_trimmed", "remote review") var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "status", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &stdout, &stderr, deps, executePlan) 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:", "Published:", "narratio.transcript.final_trimmed locked", "narratio.transcript.final_trimmed locked remote=published", "narratio.transcript.final dest=transcripts/full.json remote=published", "Stable input players:", "Stable input party:", } { if !strings.Contains(out, want) { t.Fatalf("stdout = %q, want %q", out, want) } } if strings.Contains(out, "narratio.transcript.base remote=missing") { t.Fatalf("stdout = %q, did not want catalog remote marker", out) } } func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) addPublishOutputsToPipeline(t, pipelinePath, ` outputs: - source: narratio.transcript.final_trimmed dest: transcripts/final.trimmed.json required: true `) fake := &storage.FakeBackend{ExistsErr: fmt.Errorf("exists failed")} var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "status", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } out := stdout.String() if !strings.Contains(out, "Remote publish: missing or unavailable:") { t.Fatalf("stdout = %q, want remote publish unavailable state", out) } if !strings.Contains(out, "Remote outputs:") || !strings.Contains(out, "narratio.transcript.final_trimmed remote=error") { t.Fatalf("stdout = %q, want remote output error state", out) } } func TestExecuteStatusReportsMissingRemoteCurrentStateWithoutFailing(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "status", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &stdout, &stderr, deps, executePlan) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), "Remote publish: missing or unavailable: remote current commit pointer missing") { t.Fatalf("stdout = %q, want missing remote current-state line", stdout.String()) } } 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 deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "status", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &stdout, &stderr, deps, executePlan) 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 commit pointer missing") { t.Fatalf("stdout = %q, want previous readiness unavailable line", stdout.String()) } } func TestExecuteStatusDetectsMissingRequiredPreviousArtifact(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") cfg, err := config.LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, config.SessionLoadOptions{}) if err != nil { t.Fatalf("LoadWithSessionOptions() error = %v", err) } fake := &storage.FakeBackend{} seedRestorePreviousCurrentManifestOnly(t, fake, cfg) var storeInitCalls int deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "status", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &stdout, &stderr, deps, executePlan) 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 required previous-session artifact "session_recap" is not declared`) { t.Fatalf("stdout = %q, want missing required previous artifact", stdout.String()) } } func TestExecuteSessionValidateReportsPreviousStateFindingAndReturnsFindingError(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 deps := fakeCommandDependencies(fake, &storeInitCalls) var stdout bytes.Buffer var stderr bytes.Buffer code := executeWithDependencies([]string{ "session", "validate", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, }, &stdout, &stderr, deps, executePlan) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stdout.String(), "ERROR previous") { t.Fatalf("stdout = %q, want previous finding error", stdout.String()) } if !strings.Contains(stdout.String(), "remote current commit pointer missing") { t.Fatalf("stdout = %q, want missing commit pointer finding", stdout.String()) } if !strings.Contains(stderr.String(), "validation error(s)") { t.Fatalf("stderr = %q, want finding error summary", stderr.String()) } } func TestExecutePublishIgnoresRetiredSharedLockObject(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidPublishRunConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03") lockKey := sessionPrefix + "locks.yml" fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.final_trimmed\n reason: remote review\n")}) workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.trimmed.json"), `{"segments":[]}`) mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.md"), "# final\n") mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.trimmed.md"), "# final trimmed\n") m := manifest.New("2026-05-03", time.Now().UTC()) m.Campaign = "sample-campaign" for _, stageName := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "extract", "analyze"} { m.MarkStageSucceeded(stageName, time.Now().UTC(), nil) } setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(workRoot, "transcripts", "final.trimmed.json")) finalMarkdown := filepath.Join(workRoot, "transcripts", "final.md") trimmedMarkdown := filepath.Join(workRoot, "transcripts", "final.trimmed.md") finalChecksum, err := artifacts.SHA256File(finalMarkdown) if err != nil { t.Fatal(err) } trimmedChecksum, err := artifacts.SHA256File(trimmedMarkdown) if err != nil { t.Fatal(err) } m.MarkStageSucceeded("render", time.Now().UTC(), []manifest.ArtifactRecord{ {Kind: artifacts.TranscriptOutputKindFinalMarkdown, SourceID: artifacts.ArtifactTranscriptFinalMarkdown, LocalPath: finalMarkdown, ProducerRunID: "render-run", Checksum: finalChecksum}, {Kind: artifacts.TranscriptOutputKindFinalTrimmedMarkdown, SourceID: artifacts.ArtifactTranscriptFinalTrimmedMarkdown, LocalPath: trimmedMarkdown, ProducerRunID: "render-run", Checksum: trimmedChecksum}, }) if err := (&manifest.LocalStore{}).Save(context.Background(), filepath.Join(workRoot, "manifest.json"), m); err != nil { t.Fatal(err) } var stdout bytes.Buffer var storeInitCalls int err = runStageWithPipelineExecutor( context.Background(), []string{"publish", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--force"}, &stdout, func(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions) (*RunSummary, error) { return executePlanWithDependencies(ctx, cfg, plan, opts, invocationDependencies{ newObjectStore: func(context.Context, *config.Config) (storage.ObjectStore, error) { storeInitCalls++ return fake, nil }, }) }, ) if err != nil { t.Fatalf("runStageWithPipelineExecutor() error = %v", err) } if storeInitCalls != 1 { t.Fatalf("object store initialization calls = %d, want 1", storeInitCalls) } current, err := artifacts.LoadCurrentState(context.Background(), fake, sessionPrefix, artifacts.CurrentStateValidation{ValidateRunID: true}) if err != nil { t.Fatalf("LoadCurrentState() error = %v", err) } found := false for _, declaration := range current.Commit.Artifacts { if declaration.Type == artifacts.RemoteArtifactTypePublishedOutput && declaration.Source == "narratio.transcript.final_trimmed" { found = true } } if !found { t.Fatal("retired shared lock object affected publication") } } func addPublishOutputsToPipeline(t *testing.T, pipelinePath, publishYAML string) { t.Helper() data, err := os.ReadFile(pipelinePath) if err != nil { t.Fatalf("read pipeline: %v", err) } updated := strings.Replace(string(data), " upload_run: false\n", " upload_run: false\n"+publishYAML, 1) if updated == string(data) { t.Fatalf("pipeline %q did not contain publish upload_run marker", pipelinePath) } if err := os.WriteFile(pipelinePath, []byte(updated), 0o644); err != nil { t.Fatalf("write pipeline: %v", err) } } func addExtractionOutputToPipeline(t *testing.T, pipelinePath string) { t.Helper() data, err := os.ReadFile(pipelinePath) if err != nil { t.Fatal(err) } data = append(data, []byte(`notarius: enabled: true config_path: notarius.yml pipeline_id: campaign.extract outputs: encounters: lane_id: encounters media_type: application/json schema_id: encounters schema_version: "1" module_key: encounters `)...) if err := os.WriteFile(pipelinePath, data, 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(filepath.Dir(pipelinePath), "notarius.yml"), []byte("{}\n"), 0o644); err != nil { t.Fatal(err) } } func writeOperatorExtractionManifest(t *testing.T, workspaceRoot string) string { t.Helper() paths := artifacts.NewLocalStore(workspaceRoot).SessionPathsFor("sample-campaign", "2026-05-03") bundleRoot := filepath.Join(paths.ArtifactsDir, "notarius", "extract-run-1") lanePath := filepath.Join(bundleRoot, "lanes", "encounters.json") indexPath := filepath.Join(bundleRoot, "index.json") trimmedPath := filepath.Join(paths.Root, filepath.FromSlash(artifacts.TranscriptPathFinalTrimmed)) mustWriteTestFile(t, lanePath, `{"secret":"DO_NOT_PRINT"}`) mustWriteTestFile(t, indexPath, `{"lanes":[]}`) mustWriteTestFile(t, trimmedPath, `{"segments":[]}`) laneChecksum, err := artifacts.SHA256File(lanePath) if err != nil { t.Fatal(err) } indexChecksum, err := artifacts.SHA256File(indexPath) if err != nil { t.Fatal(err) } m := manifest.New("2026-05-03", time.Now().UTC()) m.Campaign = "sample-campaign" trimmedChecksum, err := artifacts.SHA256File(trimmedPath) if err != nil { t.Fatal(err) } m.Stages["trim"] = &manifest.StageRecord{ Name: "trim", Status: manifest.StatusSucceeded, Outputs: []manifest.ArtifactRecord{{ Kind: artifactmodel.TranscriptOutputKindFinalTrimmed, SourceID: artifactmodel.SourceTranscriptFinalTrimmed, LocalPath: trimmedPath, ProducerRunID: "trim-run-1", Checksum: trimmedChecksum, }}, } input, err := artifacts.ResolveExtractionInputIdentity(paths, m) if err != nil { t.Fatal(err) } m.Stages["extract"] = &manifest.StageRecord{ Name: "extract", Status: manifest.StatusSucceeded, Metadata: map[string]any{ "narratio_run_id": "extract-run-1", "bundle_root": bundleRoot, "receipt": map[string]any{"run_id": "notarius-run-1", "pipeline_id": "campaign.extract"}, "direct_input": input.Metadata(), }, Outputs: []manifest.ArtifactRecord{ { Kind: "notarius_lane", SourceID: artifacts.ExtractionArtifactSourceID("encounters"), LocalPath: lanePath, ProducerRunID: "extract-run-1", Checksum: laneChecksum, Contract: &artifactmodel.ContractMetadata{MediaType: "application/json", SchemaID: "encounters", SchemaVersion: "1", ModuleKey: "encounters"}, ExternalProvenance: &artifactmodel.ExternalProvenance{System: "notarius", RunID: "notarius-run-1", PipelineID: "campaign.extract", ArtifactID: "encounters"}, }, {Kind: "notarius_index", LocalPath: indexPath, ProducerRunID: "extract-run-1", Checksum: indexChecksum}, }, } if err := (&manifest.LocalStore{}).Save(context.Background(), paths.ManifestPath, m); err != nil { t.Fatal(err) } return lanePath } func replaceInFileOrFatal(t *testing.T, path, old, new string) { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("read %s: %v", path, err) } updated := strings.Replace(string(data), old, new, 1) if updated == string(data) { t.Fatalf("%s did not contain %q", path, old) } if err := os.WriteFile(path, []byte(updated), 0o644); err != nil { t.Fatalf("write %s: %v", path, err) } } func seedOperatorRemoteLock(t *testing.T, fake *storage.FakeBackend, sessionPrefix, source, reason string) string { t.Helper() lock, err := artifacts.NewRemoteLock(source, reason, time.Date(2026, 5, 19, 1, 2, 3, 0, time.UTC)) if err != nil { t.Fatalf("NewRemoteLock() error = %v", err) } data, err := artifacts.EncodeRemoteLock(lock) if err != nil { t.Fatalf("EncodeRemoteLock() error = %v", err) } key, err := artifacts.RemoteLockKey(sessionPrefix, source) if err != nil { t.Fatalf("RemoteLockKey() error = %v", err) } fake.SeedObject(storage.FakeObject{Key: key, Data: data}) return key } func writeValidPublishRunConfigFiles(t *testing.T, workspaceRoot string) (string, string, string) { t.Helper() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) data, err := os.ReadFile(pipelinePath) if err != nil { t.Fatalf("read pipeline: %v", err) } updated := strings.Replace(string(data), "upload_run: false", "upload_run: true", 1) if err := os.WriteFile(pipelinePath, []byte(updated), 0o644); err != nil { t.Fatalf("write pipeline: %v", err) } ctx := context.Background() cfg, err := config.LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, config.SessionLoadOptions{}) if err != nil { t.Fatalf("LoadWithSessionOptions() error = %v", err) } store := &manifest.LocalStore{} m := manifest.New("2026-05-03", nowUTC()) m.Campaign = "sample-campaign" m.RunID = "20260521T160000Z-test" for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "extract", "render", "analyze"} { m.MarkStageSucceeded(name, nowUTC(), nil) } path := artifacts.SessionManifestPathForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID) if err := store.Save(ctx, path, m); err != nil { t.Fatalf("save manifest: %v", err) } return pipelinePath, campaignPath, sessionPath }