2026-05-21 11:50:20 -05:00
package app
import (
"bytes"
"context"
2026-08-10 20:17:54 +00:00
"errors"
2026-05-21 19:13:10 -05:00
"fmt"
2026-05-21 11:50:20 -05:00
"os"
"path/filepath"
"strings"
2026-08-10 20:17:54 +00:00
"sync"
2026-05-21 11:50:20 -05:00
"testing"
2026-08-10 00:32:24 +00:00
"time"
2026-05-21 11:50:20 -05:00
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
2026-08-10 00:32:24 +00:00
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
2026-05-21 11:50:20 -05:00
"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
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { filepath . Join ( t . TempDir ( ) , "session.yml" ) } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "init" , "2026-06-07" ,
2026-05-21 11:50:20 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 11:50:20 -05:00
"--title" , "The Black Cabin" ,
"--remote" ,
} , & stdout , & stderr )
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 )
}
}
2026-05-22 11:36:57 -05:00
func TestExecuteSessionInitRemoteUsesDefaultConfigDiscovery ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , _ := writeValidConfigFiles ( t , workspaceRoot )
withDefaultPipelineCampaignConfigs ( t , pipelinePath , campaignPath )
fake := & storage . FakeBackend { }
var storeInitCalls int
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { filepath . Join ( t . TempDir ( ) , "session.yml" ) } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "init" , "2026-06-07" ,
2026-05-22 11:36:57 -05:00
"--remote" ,
} , & stdout , & stderr )
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 {
2026-05-22 22:09:17 -05:00
"session" , "init" , "2026-06-07" ,
2026-05-22 11:36:57 -05:00
"--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" )
2026-05-22 23:01:27 -05:00
if err := os . WriteFile ( explicitCampaign , [ ] byte ( ` campaign_id : explicit - campaign
2026-05-22 11:36:57 -05:00
inputs :
speakers_file : . / speakers . yml
autocorrect_file : . / autocorrect . yml
glossary_file : . / glossary . yml
2026-05-27 09:34:05 -05:00
party_file : . / party . yml
2026-05-22 11:36:57 -05:00
` ) , 0 o644 ) ; 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" )
2026-05-27 09:34:05 -05:00
mustWriteTestFile ( t , filepath . Join ( explicitDir , "players.yml" ) , "[]\n" )
2026-09-02 11:57:51 +00:00
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" )
2026-05-22 11:36:57 -05:00
fake := & storage . FakeBackend { }
var storeInitCalls int
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { filepath . Join ( t . TempDir ( ) , "session.yml" ) } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "init" , "2026-06-07" ,
2026-05-22 11:36:57 -05:00
"--config" , defaultPipeline ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , explicitCampaign ,
2026-05-22 11:36:57 -05:00
"--remote" ,
} , & stdout , & stderr )
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" )
}
2026-05-22 22:09:17 -05:00
if ! strings . Contains ( stderr . String ( ) , "session init: session_id is required" ) {
2026-05-22 11:36:57 -05:00
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
2026-05-22 22:09:17 -05:00
code := Execute ( [ ] string { "session" , "init" , "2026-06-07" , "--remote" } , & stdout , & stderr )
2026-05-22 11:36:57 -05:00
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 ( )
2026-08-10 18:24:29 +00:00
mustWriteSecretFile ( t , filepath . Join ( secretsDir , accessKeyEnv ) , "test-key-id\n" )
mustWriteSecretFile ( t , filepath . Join ( secretsDir , secretKeyEnv ) , "test-secret\n" )
2026-05-22 11:36:57 -05:00
addSecretsToPipelineConfig ( t , pipelinePath , secretsDir , accessKeyEnv , secretKeyEnv )
fake := & storage . FakeBackend { }
origStoreFn := newObjectStoreFromConfigFn
newObjectStoreFromConfigFn = 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
2026-05-22 22:09:17 -05:00
code := Execute ( [ ] string { "session" , "init" , "2026-06-07" , "--remote" } , & stdout , & stderr )
2026-05-22 11:36:57 -05:00
if code != 0 {
t . Fatalf ( "exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
}
2026-05-22 17:38:23 -05:00
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 {
2026-05-22 22:09:17 -05:00
"session" , "init" , "2026-06-07" ,
2026-05-22 17:38:23 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-22 17:38:23 -05:00
"--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
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { filepath . Join ( t . TempDir ( ) , "session.yml" ) } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "init" , "2026-06-07" ,
2026-05-22 17:38:23 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-22 17:38:23 -05:00
"--remote" ,
} , & stdout , & stderr )
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 , 0 o755 ) ; 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
` ) , 0 o644 ) ; 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 {
2026-05-22 22:09:17 -05:00
"session" , "init" , "2026-06-07" ,
2026-05-22 17:38:23 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-22 17:38:23 -05:00
"--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 {
2026-05-22 22:09:17 -05:00
"session" , "init" , "2026-06-07" ,
2026-05-22 17:38:23 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-22 17:38:23 -05:00
"--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 {
2026-05-22 22:09:17 -05:00
"session" , "init" , "2026-06-07" ,
2026-05-22 17:38:23 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-22 17:38:23 -05:00
"--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 {
2026-05-22 22:09:17 -05:00
"session" , "init" , "2026-06-07" ,
2026-05-22 17:38:23 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-22 17:38:23 -05:00
"--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 ( ) )
}
}
2026-05-21 19:13:10 -05:00
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 ( )
2026-08-10 18:24:29 +00:00
mustWriteSecretFile ( t , filepath . Join ( secretsDir , accessKeyEnv ) , "test-key-id\n" )
mustWriteSecretFile ( t , filepath . Join ( secretsDir , secretKeyEnv ) , "test-secret\n" )
2026-05-21 19:13:10 -05:00
addSecretsToPipelineConfig ( t , pipelinePath , secretsDir , accessKeyEnv , secretKeyEnv )
if err := os . WriteFile ( sessionPath , [ ] byte ( ` session_id : 2026 - 05 - 03
inputs :
audio_s3 :
prefix : audio /
` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write session: %v" , err )
}
fake := & storage . FakeBackend { }
2026-05-23 04:42:08 +00:00
audioKey := artifacts . S3PublishedOutputKey ( artifacts . S3AudioPrefix ( artifacts . S3SessionPrefix ( "dnd" , "sample-campaign" , "2026-05-03" ) , "audio/" ) , "alice.flac" )
2026-05-21 19:13:10 -05:00
fake . SeedObject ( storage . FakeObject { Key : audioKey , Data : [ ] byte ( "audio" ) } )
origStoreFn := newObjectStoreFromConfigFn
newObjectStoreFromConfigFn = 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
2026-05-22 23:01:27 -05:00
code := Execute ( [ ] string { "session" , "validate" , "2026-05-03" , "--config" , pipelinePath , "--campaign-file" , campaignPath , "--session" , sessionPath } , & stdout , & stderr )
2026-05-21 19:13:10 -05:00
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 ( ) )
}
2026-05-27 09:34:05 -05:00
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 ( ) )
}
2026-05-21 19:13:10 -05:00
}
2026-08-29 15:03:44 +00:00
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 ) , 0 o644 ) ; 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
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { sessionPath } )
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 := Execute ( validateArgs , & stdout , & stderr ) ; 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 := Execute ( statusArgs , & stdout , & stderr ) ; 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 := Execute ( validateArgs , & stdout , & stderr ) ; 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 ( ) )
}
}
2026-05-21 19:36:14 -05:00
func TestExecuteLocksAddListAndRemoveUseRemoteLockStore ( t * testing . T ) {
2026-05-21 11:50:20 -05:00
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
fake := & storage . FakeBackend { }
var storeInitCalls int
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { sessionPath } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "locks" , "add" , "2026-05-03" , "narratio.transcript.final_trimmed" ,
2026-05-21 11:50:20 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 11:50:20 -05:00
"--session" , sessionPath ,
"--reason" , "manual edit" ,
} , & stdout , & stderr )
if code != 0 {
2026-05-21 19:36:14 -05:00
t . Fatalf ( "locks add exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
2026-05-21 11:50:20 -05:00
}
key := artifacts . S3SessionLocksKey ( artifacts . S3SessionPrefix ( "dnd" , "sample-campaign" , "2026-05-03" ) )
obj , ok := fake . Objects [ key ]
if ! ok {
t . Fatalf ( "remote locks key %q not uploaded" , key )
}
2026-05-22 19:05:23 -05:00
if ! strings . Contains ( string ( obj . Data ) , "source: narratio.transcript.final_trimmed" ) || ! strings . Contains ( string ( obj . Data ) , "reason: manual edit" ) {
2026-05-21 11:50:20 -05:00
t . Fatalf ( "lock store data = %q" , string ( obj . Data ) )
}
stdout . Reset ( )
stderr . Reset ( )
code = Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "locks" , "2026-05-03" ,
2026-05-21 11:50:20 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 11:50:20 -05:00
"--session" , sessionPath ,
2026-05-21 19:36:14 -05:00
} , & stdout , & stderr )
if code != 0 {
t . Fatalf ( "locks list exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
2026-05-22 19:05:23 -05:00
if ! strings . Contains ( stdout . String ( ) , "- narratio.transcript.final_trimmed origin=remote" ) {
2026-05-21 19:36:14 -05:00
t . Fatalf ( "stdout = %q, want remote lock" , stdout . String ( ) )
}
stdout . Reset ( )
stderr . Reset ( )
code = Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "locks" , "remove" , "2026-05-03" , "narratio.transcript.final_trimmed" ,
2026-05-21 19:36:14 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 19:36:14 -05:00
"--session" , sessionPath ,
2026-05-21 11:50:20 -05:00
} , & stdout , & stderr )
if code != 0 {
2026-05-21 19:36:14 -05:00
t . Fatalf ( "locks remove exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
2026-05-21 11:50:20 -05:00
}
2026-08-09 23:30:32 +00:00
store , err := config . LoadPublishLockStoreBytes ( "locks.yml" , fake . Objects [ key ] . Data , nil , nil )
2026-05-21 11:50:20 -05:00
if err != nil {
2026-05-23 04:42:08 +00:00
t . Fatalf ( "LoadPublishLockStoreBytes() error = %v" , err )
2026-05-21 11:50:20 -05:00
}
if len ( store . Locks ) != 0 {
2026-05-21 19:36:14 -05:00
t . Fatalf ( "locks after remove = %#v, want empty" , store . Locks )
2026-05-21 11:50:20 -05:00
}
2026-05-21 19:36:14 -05:00
if storeInitCalls != 3 {
t . Fatalf ( "object store init calls = %d, want 3" , storeInitCalls )
}
}
2026-08-10 20:17:54 +00:00
func TestMutateRemoteLockStoreRetainsConcurrentUpdates ( t * testing . T ) {
cfg := & config . Config {
Pipeline : & config . PipelineConfig { Storage : config . StorageConfig { S3 : & config . StorageS3Config { Bucket : "bucket" , RootPrefix : "root" } } } ,
Session : & config . SessionConfig { Campaign : "campaign" , SessionID : "session" } ,
}
fake := & storage . FakeBackend { }
arrived := make ( chan struct { } , 2 )
release := make ( chan struct { } )
var hookMu sync . Mutex
hookCalls := 0
fake . UploadHook = func ( storage . FakeUploadCall ) error {
hookMu . Lock ( )
hookCalls ++
call := hookCalls
hookMu . Unlock ( )
if call <= 2 {
arrived <- struct { } { }
<- release
}
return nil
}
mutate := func ( source string ) error {
return mutateRemoteLockStore ( context . Background ( ) , cfg , fake , func ( lockStore * config . PublishLockStore ) error {
set := lockSourceSet ( lockStore . Locks )
set [ source ] = config . PublishLockRule { Source : source }
lockStore . Locks = lockMapValues ( set )
return nil
} )
}
errs := make ( chan error , 2 )
go func ( ) { errs <- mutate ( "narratio.transcript.final" ) } ( )
go func ( ) { errs <- mutate ( "narratio.transcript.final_trimmed" ) } ( )
<- arrived
<- arrived
close ( release )
if err := <- errs ; err != nil {
t . Fatalf ( "first concurrent mutation error = %v" , err )
}
if err := <- errs ; err != nil {
t . Fatalf ( "second concurrent mutation error = %v" , err )
}
locks , _ , _ , err := loadRemoteLockStore ( context . Background ( ) , cfg , fake )
if err != nil {
t . Fatalf ( "loadRemoteLockStore() error = %v" , err )
}
if len ( locks . Locks ) != 2 || locks . Locks [ 0 ] . Source != "narratio.transcript.final" || locks . Locks [ 1 ] . Source != "narratio.transcript.final_trimmed" {
t . Fatalf ( "remote locks = %#v, want both concurrent updates" , locks . Locks )
}
}
2026-08-11 03:43:18 +00:00
func TestLoadRemoteLockStoreAcceptsExactLimitAndRejectsLimitPlusOne ( t * testing . T ) {
cfg := & config . Config {
Pipeline : & config . PipelineConfig { Storage : config . StorageConfig { S3 : & config . StorageS3Config { Bucket : "bucket" , RootPrefix : "root" } } } ,
Session : & config . SessionConfig { Campaign : "campaign" , SessionID : "session" } ,
}
key , err := remoteLocksKey ( cfg )
if err != nil {
t . Fatalf ( "remoteLocksKey() error = %v" , err )
}
encoded , err := config . MarshalPublishLockStore ( & config . PublishLockStore { } )
if err != nil {
t . Fatalf ( "MarshalPublishLockStore() error = %v" , err )
}
exact := append ( append ( [ ] byte ( nil ) , encoded ... ) , bytes . Repeat ( [ ] byte ( " " ) , int ( MaxRemoteLockStoreBytes ) - len ( encoded ) ) ... )
store := & storage . FakeBackend { }
store . SeedObject ( storage . FakeObject { Key : key , Data : exact , ETag : "lock-generation" } )
locks , gotKey , generation , err := loadRemoteLockStore ( context . Background ( ) , cfg , store )
if err != nil {
t . Fatalf ( "loadRemoteLockStore() exact-limit error = %v" , err )
}
if locks == nil || gotKey != key || generation != "lock-generation" {
t . Fatalf ( "loadRemoteLockStore() = (%#v, %q, %q), want decoded locks and opened generation" , locks , gotKey , generation )
}
if len ( store . Downloads ) != 0 || len ( store . Reads ) != 1 || store . Reads [ 0 ] . Key != key {
t . Fatalf ( "lock transfers reads=%#v downloads=%#v, want one direct read" , store . Reads , store . Downloads )
}
store . SeedObject ( storage . FakeObject { Key : key , Data : append ( exact , ' ' ) , ETag : "new-generation" } )
_ , _ , _ , err = loadRemoteLockStore ( context . Background ( ) , cfg , store )
if err == nil || ! strings . Contains ( err . Error ( ) , "remote lock control object" ) || ! strings . Contains ( err . Error ( ) , key ) || ! strings . Contains ( err . Error ( ) , fmt . Sprint ( MaxRemoteLockStoreBytes ) ) {
t . Fatalf ( "loadRemoteLockStore() limit-plus-one error = %v, want category, key, and limit" , err )
}
}
func TestLoadRemoteLockStoreRejectsMalformedYAML ( t * testing . T ) {
cfg := & config . Config {
Pipeline : & config . PipelineConfig { Storage : config . StorageConfig { S3 : & config . StorageS3Config { Bucket : "bucket" , RootPrefix : "root" } } } ,
Session : & config . SessionConfig { Campaign : "campaign" , SessionID : "session" } ,
}
key , err := remoteLocksKey ( cfg )
if err != nil {
t . Fatalf ( "remoteLocksKey() error = %v" , err )
}
store := & storage . FakeBackend { }
store . SeedObject ( storage . FakeObject { Key : key , Data : [ ] byte ( "locks: [\n" ) , ETag : "lock-generation" } )
if _ , _ , _ , err := loadRemoteLockStore ( context . Background ( ) , cfg , store ) ; err == nil {
t . Fatal ( "loadRemoteLockStore() error = nil, want malformed YAML failure" )
}
}
2026-08-10 20:17:54 +00:00
func TestMutateRemoteLockStoreHonorsCancellation ( t * testing . T ) {
cfg := & config . Config {
Pipeline : & config . PipelineConfig { Storage : config . StorageConfig { S3 : & config . StorageS3Config { Bucket : "bucket" , RootPrefix : "root" } } } ,
Session : & config . SessionConfig { Campaign : "campaign" , SessionID : "session" } ,
}
ctx , cancel := context . WithCancel ( context . Background ( ) )
cancel ( )
err := mutateRemoteLockStore ( ctx , cfg , & storage . FakeBackend { } , func ( * config . PublishLockStore ) error { return nil } )
if ! errors . Is ( err , context . Canceled ) {
t . Fatalf ( "mutateRemoteLockStore() error = %v, want context cancellation" , err )
}
}
2026-05-21 19:36:14 -05:00
func TestExecuteLocksAddDuplicateRequiresForce ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
fake := & storage . FakeBackend { }
var storeInitCalls int
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { sessionPath } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "locks" , "add" , "2026-05-03" , "narratio.transcript.final_trimmed" ,
2026-05-21 19:36:14 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 19:36:14 -05:00
"--session" , sessionPath ,
"--reason" , "first" ,
} , & stdout , & stderr )
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 {
2026-05-22 22:09:17 -05:00
"session" , "locks" , "add" , "2026-05-03" , "narratio.transcript.final_trimmed" ,
2026-05-21 19:36:14 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 19:36:14 -05:00
"--session" , sessionPath ,
"--reason" , "second" ,
} , & stdout , & stderr )
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 = Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "locks" , "add" , "2026-05-03" , "narratio.transcript.final_trimmed" ,
2026-05-21 19:36:14 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 19:36:14 -05:00
"--session" , sessionPath ,
"--reason" , "second" ,
"--force" ,
} , & stdout , & stderr )
if code != 0 {
t . Fatalf ( "forced locks add exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
key := artifacts . S3SessionLocksKey ( artifacts . S3SessionPrefix ( "dnd" , "sample-campaign" , "2026-05-03" ) )
if ! strings . Contains ( string ( fake . Objects [ key ] . Data ) , "reason: second" ) {
t . Fatalf ( "lock store data = %q, want updated reason" , string ( fake . Objects [ key ] . Data ) )
}
}
func TestExecuteLocksRequireSessionID ( t * testing . T ) {
tests := [ ] struct {
name string
args [ ] string
want string
} {
2026-05-22 22:09:17 -05:00
{ "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" } ,
2026-05-21 19:36:14 -05:00
}
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 )
}
} )
}
}
2026-05-23 16:03:20 +00:00
func TestExecuteLocksMutationRejectsSessionIDMismatch ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
fake := & storage . FakeBackend { }
var storeInitCalls int
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { sessionPath } )
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 := Execute ( tt . args , & stdout , & stderr )
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 ( ) )
}
} )
}
}
2026-05-21 19:36:14 -05:00
func TestExecuteLocksCannotModifyStaticLocks ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
2026-05-23 13:39:15 +00:00
addStaticPublishLockToPipelineConfig ( t , pipelinePath , "narratio.transcript.final_trimmed" )
2026-05-21 19:36:14 -05:00
fake := & storage . FakeBackend { }
var storeInitCalls int
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { sessionPath } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "locks" , "add" , "2026-05-03" , "narratio.transcript.final_trimmed" ,
2026-05-21 19:36:14 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 19:36:14 -05:00
"--session" , sessionPath ,
} , & stdout , & stderr )
if code == 0 {
t . Fatal ( "locks add static lock exit code = 0, want non-zero" )
}
if ! strings . Contains ( stderr . String ( ) , "locked by pipeline config" ) {
t . Fatalf ( "stderr = %q, want static lock error" , stderr . String ( ) )
}
stdout . Reset ( )
stderr . Reset ( )
code = Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "locks" , "remove" , "2026-05-03" , "narratio.transcript.final_trimmed" ,
2026-05-21 19:36:14 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 19:36:14 -05:00
"--session" , sessionPath ,
} , & stdout , & stderr )
if code == 0 {
t . Fatal ( "locks remove static lock exit code = 0, want non-zero" )
}
if ! strings . Contains ( stderr . String ( ) , "locked by pipeline config" ) {
t . Fatalf ( "stderr = %q, want static lock error" , 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
2026-05-22 19:05:23 -05:00
code := Execute ( [ ] string { cmd , "narratio.transcript.final_trimmed" } , & stdout , & stderr )
2026-05-21 19:36:14 -05:00
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 ( ) )
}
} )
2026-05-21 11:50:20 -05:00
}
}
2026-05-22 11:36:57 -05:00
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
} )
2026-05-22 23:01:27 -05:00
_ = campaignPath
2026-05-22 11:36:57 -05:00
}
2026-05-22 17:38:23 -05:00
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 ) , 0 o644 ) ; 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 ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write campaign config: %v" , err )
}
}
2026-05-23 13:39:15 +00:00
func TestExecuteArtifactsListRemoteReportsPublishedAvailability ( t * testing . T ) {
2026-05-21 11:50:20 -05:00
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
2026-05-23 13:39:15 +00:00
addPublishOutputsToPipeline ( t , pipelinePath , `
2026-05-23 04:30:45 +00:00
outputs :
2026-05-22 19:05:23 -05:00
- source : narratio . transcript . final_trimmed
dest : transcripts / final . trimmed . json
2026-05-21 21:20:41 -05:00
required : true
` )
2026-05-21 11:50:20 -05:00
fake := & storage . FakeBackend { }
2026-05-23 04:42:08 +00:00
trimmedKey := artifacts . S3PublishedOutputKey (
2026-05-21 11:50:20 -05:00
artifacts . S3SessionPrefix ( "dnd" , "sample-campaign" , "2026-05-03" ) ,
2026-05-22 19:05:23 -05:00
"transcripts/final.trimmed.json" ,
2026-05-21 11:50:20 -05:00
)
fake . SeedObject ( storage . FakeObject { Key : trimmedKey , Data : [ ] byte ( ` { "segments":[]} ` ) } )
var storeInitCalls int
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { sessionPath } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "artifacts" , "2026-05-03" ,
2026-05-21 11:50:20 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 11:50:20 -05:00
"--session" , sessionPath ,
"--remote" ,
} , & stdout , & stderr )
if code != 0 {
t . Fatalf ( "exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
2026-05-23 04:42:08 +00:00
if ! strings . Contains ( stdout . String ( ) , "narratio.transcript.final_trimmed remote=published" ) {
2026-05-23 13:39:15 +00:00
t . Fatalf ( "stdout = %q, want published remote availability" , stdout . String ( ) )
2026-05-21 11:50:20 -05:00
}
}
2026-08-10 00:32:24 +00:00
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 { }
publishedKey := artifacts . S3PublishedOutputKey (
artifacts . S3SessionPrefix ( "dnd" , "sample-campaign" , "2026-05-03" ) ,
"artifacts/encounters.json" ,
)
fake . SeedObject ( storage . FakeObject { Key : publishedKey , Data : [ ] byte ( ` { "secret":"DO_NOT_PRINT"} ` ) } )
var storeInitCalls int
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { sessionPath } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
"session" , "artifacts" , "2026-05-03" ,
"--config" , pipelinePath ,
"--campaign-file" , campaignPath ,
"--session" , sessionPath ,
"--remote" ,
} , & stdout , & stderr )
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 = Execute ( [ ] string {
"session" , "artifacts" , "2026-05-03" ,
"--config" , pipelinePath ,
"--campaign-file" , campaignPath ,
"--session" , sessionPath ,
} , & stdout , & stderr )
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 ( ) )
}
}
2026-05-23 13:39:15 +00:00
func TestExecuteArtifactsListRemoteUsesPublishOutputDestinations ( t * testing . T ) {
2026-05-21 21:00:51 -05:00
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
2026-05-23 13:39:15 +00:00
addPublishOutputsToPipeline ( t , pipelinePath , `
2026-05-23 04:30:45 +00:00
outputs :
2026-05-22 19:05:23 -05:00
- source : narratio . transcript . final
2026-05-21 21:00:51 -05:00
dest : transcripts / full . json
required : true
- source : narratio . bounds . session
dest : transcripts / bounds . json
required : true
` )
fake := & storage . FakeBackend { }
sessionPrefix := artifacts . S3SessionPrefix ( "dnd" , "sample-campaign" , "2026-05-03" )
2026-05-23 04:42:08 +00:00
fake . SeedObject ( storage . FakeObject { Key : artifacts . S3PublishedOutputKey ( sessionPrefix , "transcripts/full.json" ) , Data : [ ] byte ( ` { "segments":[]} ` ) } )
fake . SeedObject ( storage . FakeObject { Key : artifacts . S3PublishedOutputKey ( sessionPrefix , "transcripts/bounds.json" ) , Data : [ ] byte ( ` { } ` ) } )
2026-05-21 21:00:51 -05:00
var storeInitCalls int
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { sessionPath } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "artifacts" , "2026-05-03" ,
2026-05-21 21:00:51 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 21:00:51 -05:00
"--session" , sessionPath ,
"--remote" ,
} , & stdout , & stderr )
if code != 0 {
t . Fatalf ( "exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
out := stdout . String ( )
2026-05-21 21:20:41 -05:00
for _ , unwanted := range [ ] string {
2026-05-22 19:05:23 -05:00
"narratio.transcript.final remote=missing" ,
2026-05-21 21:00:51 -05:00
"narratio.bounds.session remote=missing" ,
2026-05-21 21:20:41 -05:00
} {
if strings . Contains ( out , unwanted ) {
t . Fatalf ( "stdout = %q, did not want catalog remote marker %q" , out , unwanted )
}
}
for _ , want := range [ ] string {
2026-05-23 04:42:08 +00:00
"narratio.transcript.final dest=transcripts/full.json remote=published" ,
"narratio.bounds.session dest=transcripts/bounds.json remote=published" ,
2026-05-21 21:00:51 -05:00
} {
if ! strings . Contains ( out , want ) {
t . Fatalf ( "stdout = %q, want %q" , out , want )
}
}
}
2026-05-21 20:49:28 -05:00
func TestExecuteStatusReportsRemoteArtifactCatalog ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
2026-05-23 13:39:15 +00:00
addPublishOutputsToPipeline ( t , pipelinePath , `
2026-05-23 04:30:45 +00:00
outputs :
2026-05-22 19:05:23 -05:00
- source : narratio . transcript . final_trimmed
dest : transcripts / final . trimmed . json
2026-05-21 21:00:51 -05:00
required : true
2026-05-22 19:05:23 -05:00
- source : narratio . transcript . final
2026-05-21 21:00:51 -05:00
dest : transcripts / full . json
required : true
` )
2026-05-21 20:49:28 -05:00
fake := & storage . FakeBackend { }
sessionPrefix := artifacts . S3SessionPrefix ( "dnd" , "sample-campaign" , "2026-05-03" )
2026-09-02 13:47:20 +00:00
manifestKey := artifacts . S3RunSessionManifestKey ( sessionPrefix , "20260519T010203Z-a1b2c3d4" )
runIDKey := artifacts . S3CurrentCommitPointerKey ( sessionPrefix )
2026-05-23 04:42:08 +00:00
trimmedKey := artifacts . S3PublishedOutputKey ( sessionPrefix , "transcripts/final.trimmed.json" )
fullKey := artifacts . S3PublishedOutputKey ( sessionPrefix , "transcripts/full.json" )
2026-05-21 20:49:28 -05:00
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":[]} ` ) } )
2026-05-21 21:00:51 -05:00
fake . SeedObject ( storage . FakeObject { Key : fullKey , Data : [ ] byte ( ` { "segments":[]} ` ) } )
2026-05-22 19:05:23 -05:00
fake . SeedObject ( storage . FakeObject { Key : lockKey , Data : [ ] byte ( "locks:\n - source: narratio.transcript.final_trimmed\n reason: remote review\n" ) } )
2026-05-21 20:49:28 -05:00
var storeInitCalls int
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { sessionPath } )
var stdout bytes . Buffer
var stderr bytes . Buffer
code := Execute ( [ ] string {
2026-05-22 22:09:17 -05:00
"session" , "status" , "2026-05-03" ,
2026-05-21 20:49:28 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 20:49:28 -05:00
"--session" , sessionPath ,
} , & 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:" ,
2026-05-23 04:42:08 +00:00
"Published:" ,
2026-05-22 19:05:23 -05:00
"narratio.transcript.final_trimmed locked" ,
2026-05-23 04:42:08 +00:00
"narratio.transcript.final_trimmed locked remote=published" ,
"narratio.transcript.final dest=transcripts/full.json remote=published" ,
2026-05-27 09:34:05 -05:00
"Stable input players:" ,
"Stable input party:" ,
2026-05-21 20:49:28 -05:00
} {
if ! strings . Contains ( out , want ) {
t . Fatalf ( "stdout = %q, want %q" , out , want )
}
}
2026-05-22 19:05:23 -05:00
if strings . Contains ( out , "narratio.transcript.base remote=missing" ) {
2026-05-21 21:20:41 -05:00
t . Fatalf ( "stdout = %q, did not want catalog remote marker" , out )
}
2026-05-21 20:49:28 -05:00
}
func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
2026-05-23 13:39:15 +00:00
addPublishOutputsToPipeline ( t , pipelinePath , `
2026-05-23 04:30:45 +00:00
outputs :
2026-05-22 19:05:23 -05:00
- source : narratio . transcript . final_trimmed
dest : transcripts / final . trimmed . json
2026-05-21 21:20:41 -05:00
required : true
` )
2026-05-21 20:49:28 -05:00
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 {
2026-05-22 22:09:17 -05:00
"session" , "status" , "2026-05-03" ,
2026-05-21 20:49:28 -05:00
"--config" , pipelinePath ,
2026-05-22 23:01:27 -05:00
"--campaign-file" , campaignPath ,
2026-05-21 20:49:28 -05:00
"--session" , sessionPath ,
} , & stdout , & stderr )
if code != 0 {
t . Fatalf ( "exit code = %d, want 0; stderr=%q" , code , stderr . String ( ) )
}
out := stdout . String ( )
2026-05-23 04:42:08 +00:00
if ! strings . Contains ( out , "Remote publish: missing or unavailable:" ) {
2026-05-23 12:43:50 +00:00
t . Fatalf ( "stdout = %q, want remote publish unavailable state" , out )
2026-05-21 20:49:28 -05:00
}
2026-05-22 19:05:23 -05:00
if ! strings . Contains ( out , "Remote outputs:" ) || ! strings . Contains ( out , "narratio.transcript.final_trimmed remote=error" ) {
2026-05-21 20:49:28 -05:00
t . Fatalf ( "stdout = %q, want remote output error state" , out )
}
}
2026-05-23 13:56:53 +00:00
func TestExecuteStatusReportsMissingRemoteCurrentStateWithoutFailing ( t * testing . T ) {
workspaceRoot := t . TempDir ( )
pipelinePath , campaignPath , sessionPath := writeValidConfigFiles ( t , workspaceRoot )
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 ( ) )
}
2026-09-02 13:47:20 +00:00
if ! strings . Contains ( stdout . String ( ) , "Remote publish: missing or unavailable: remote current commit pointer missing" ) {
2026-05-23 13:56:53 +00:00
t . Fatalf ( "stdout = %q, want missing remote current-state line" , stdout . String ( ) )
}
}
2026-05-23 16:00:31 +00:00
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 ( ) )
}
2026-09-02 13:47:20 +00:00
if ! strings . Contains ( stdout . String ( ) , "Previous-session artifacts: unavailable: remote current commit pointer missing" ) {
2026-05-23 16:00:31 +00:00
t . Fatalf ( "stdout = %q, want previous readiness unavailable line" , stdout . String ( ) )
}
}
2026-08-10 21:20:59 +00:00
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
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 ( ) )
}
2026-09-02 13:53:23 +00:00
if ! strings . Contains ( stdout . String ( ) , ` Previous-session artifacts: unavailable: remote required previous-session artifact "session_recap" is not declared ` ) {
2026-08-10 21:20:59 +00:00
t . Fatalf ( "stdout = %q, want missing required previous artifact" , stdout . String ( ) )
}
}
2026-05-23 13:56:53 +00:00
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
restoreAppConfigTestGlobals ( t , fake , & storeInitCalls , [ ] string { sessionPath } )
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 )
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 ( ) )
}
2026-09-02 13:47:20 +00:00
if ! strings . Contains ( stdout . String ( ) , "remote current commit pointer missing" ) {
t . Fatalf ( "stdout = %q, want missing commit pointer finding" , stdout . String ( ) )
2026-05-23 13:56:53 +00:00
}
if ! strings . Contains ( stderr . String ( ) , "validation error(s)" ) {
t . Fatalf ( "stderr = %q, want finding error summary" , stderr . String ( ) )
}
}
2026-05-23 13:39:15 +00:00
func TestExecutePublishLoadsRemoteLocks ( t * testing . T ) {
2026-05-21 11:50:20 -05:00
workspaceRoot := t . TempDir ( )
2026-05-23 13:39:15 +00:00
pipelinePath , campaignPath , sessionPath := writeValidPublishRunConfigFiles ( t , workspaceRoot )
2026-05-21 11:50:20 -05:00
fake := & storage . FakeBackend { }
lockKey := artifacts . S3SessionLocksKey ( artifacts . S3SessionPrefix ( "dnd" , "sample-campaign" , "2026-05-03" ) )
2026-05-22 19:05:23 -05:00
fake . SeedObject ( storage . FakeObject { Key : lockKey , Data : [ ] byte ( "locks:\n - source: narratio.transcript.final_trimmed\n reason: remote review\n" ) } )
2026-05-21 11:50:20 -05:00
workRoot := filepath . Join ( workspaceRoot , "work" , "sample-campaign" , "2026-05-03" )
2026-05-22 19:05:23 -05:00
mustWriteTestFile ( t , filepath . Join ( workRoot , "transcripts" , "final.trimmed.json" ) , ` { "segments":[]} ` )
2026-05-25 00:46:27 +00:00
mustWriteTestFile ( t , filepath . Join ( workRoot , "transcripts" , "final.md" ) , "# final\n" )
mustWriteTestFile ( t , filepath . Join ( workRoot , "transcripts" , "final.trimmed.md" ) , "# final trimmed\n" )
2026-09-02 14:09:32 +00:00
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 )
}
2026-05-21 11:50:20 -05:00
var stdout bytes . Buffer
2026-09-04 13:01:00 +00:00
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 )
2026-05-21 11:50:20 -05:00
}
2026-05-23 13:39:15 +00:00
publishedKey := artifacts . S3PublishedOutputKey ( artifacts . S3SessionPrefix ( "dnd" , "sample-campaign" , "2026-05-03" ) , "transcripts/final.trimmed.json" )
if _ , ok := fake . Objects [ publishedKey ] ; ok {
t . Fatalf ( "locked published key %q was uploaded" , publishedKey )
2026-05-21 11:50:20 -05:00
}
}
2026-05-23 13:39:15 +00:00
func addPublishOutputsToPipeline ( t * testing . T , pipelinePath , publishYAML string ) {
2026-05-21 21:00:51 -05:00
t . Helper ( )
data , err := os . ReadFile ( pipelinePath )
if err != nil {
t . Fatalf ( "read pipeline: %v" , err )
}
2026-05-23 13:39:15 +00:00
updated := strings . Replace ( string ( data ) , " upload_run: false\n" , " upload_run: false\n" + publishYAML , 1 )
2026-05-21 21:00:51 -05:00
if updated == string ( data ) {
2026-05-23 13:39:15 +00:00
t . Fatalf ( "pipeline %q did not contain publish upload_run marker" , pipelinePath )
2026-05-21 21:00:51 -05:00
}
if err := os . WriteFile ( pipelinePath , [ ] byte ( updated ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write pipeline: %v" , err )
}
}
2026-08-10 00:32:24 +00:00
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 , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
if err := os . WriteFile ( filepath . Join ( filepath . Dir ( pipelinePath ) , "notarius.yml" ) , [ ] byte ( "{}\n" ) , 0 o644 ) ; 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" )
2026-08-10 22:15:53 +00:00
trimmedPath := filepath . Join ( paths . Root , filepath . FromSlash ( artifacts . TranscriptPathFinalTrimmed ) )
2026-08-10 00:32:24 +00:00
mustWriteTestFile ( t , lanePath , ` { "secret":"DO_NOT_PRINT"} ` )
mustWriteTestFile ( t , indexPath , ` { "lanes":[]} ` )
2026-08-10 22:15:53 +00:00
mustWriteTestFile ( t , trimmedPath , ` { "segments":[]} ` )
2026-08-10 00:32:24 +00:00
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"
2026-09-02 14:09:32 +00:00
trimmedChecksum , err := artifacts . SHA256File ( trimmedPath )
if err != nil {
t . Fatal ( err )
}
2026-08-10 22:15:53 +00:00
m . Stages [ "trim" ] = & manifest . StageRecord {
Name : "trim" , Status : manifest . StatusSucceeded ,
Outputs : [ ] manifest . ArtifactRecord { {
2026-09-02 14:09:32 +00:00
Kind : artifactmodel . TranscriptOutputKindFinalTrimmed , SourceID : artifactmodel . SourceTranscriptFinalTrimmed , LocalPath : trimmedPath , ProducerRunID : "trim-run-1" , Checksum : trimmedChecksum ,
2026-08-10 22:15:53 +00:00
} } ,
}
input , err := artifacts . ResolveExtractionInputIdentity ( paths , m )
if err != nil {
t . Fatal ( err )
}
2026-08-10 00:32:24 +00:00
m . Stages [ "extract" ] = & manifest . StageRecord {
Name : "extract" , Status : manifest . StatusSucceeded ,
Metadata : map [ string ] any {
"narratio_run_id" : "extract-run-1" , "bundle_root" : bundleRoot ,
2026-08-10 22:15:53 +00:00
"receipt" : map [ string ] any { "run_id" : "notarius-run-1" , "pipeline_id" : "campaign.extract" } ,
"direct_input" : input . Metadata ( ) ,
2026-08-10 00:32:24 +00:00
} ,
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
}
2026-05-23 13:56:53 +00:00
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 ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write %s: %v" , path , err )
}
}
2026-05-23 13:39:15 +00:00
func writeValidPublishRunConfigFiles ( t * testing . T , workspaceRoot string ) ( string , string , string ) {
2026-05-21 11:50:20 -05:00
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 ) , 0 o644 ) ; 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"
2026-08-10 00:14:46 +00:00
for _ , name := range [ ] string { "prepare" , "transcribe" , "merge" , "polish" , "normalize" , "trim" , "extract" , "render" , "analyze" } {
2026-05-21 11:50:20 -05:00
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
}
2026-05-21 19:36:14 -05:00
2026-05-23 13:39:15 +00:00
func addStaticPublishLockToPipelineConfig ( t * testing . T , pipelinePath , source string ) {
2026-05-21 19:36:14 -05:00
t . Helper ( )
data , err := os . ReadFile ( pipelinePath )
if err != nil {
t . Fatalf ( "read pipeline: %v" , err )
}
updated := strings . Replace (
string ( data ) ,
2026-05-23 04:30:45 +00:00
"publish:\n enabled: true\n upload_run: false\n" ,
"publish:\n enabled: true\n upload_run: false\n locks:\n - source: " + source + "\n reason: static review\n" ,
2026-05-21 19:36:14 -05:00
1 ,
)
if updated == string ( data ) {
2026-05-23 13:39:15 +00:00
t . Fatalf ( "publish section not found in pipeline config" )
2026-05-21 19:36:14 -05:00
}
if err := os . WriteFile ( pipelinePath , [ ] byte ( updated ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write pipeline: %v" , err )
}
}