Add configuration source reporting

This commit is contained in:
2026-08-30 14:56:22 +00:00
parent a102db36af
commit dde7f76ecb
11 changed files with 654 additions and 18 deletions

View File

@@ -21,11 +21,12 @@ func TestConfigCommandHelpAndDispatch(t *testing.T) {
want string
code int
}{
{name: "top level help", args: []string{"config", "--help"}, want: "Usage: narratio config <validate|show>"},
{name: "top level help", args: []string{"config", "--help"}, want: "Usage: narratio config <validate|show|sources>"},
{name: "validate help", args: []string{"config", "validate", "--help"}, want: "Usage: narratio config validate"},
{name: "show help", args: []string{"config", "show", "--help"}, want: "Usage: narratio config show"},
{name: "unknown", args: []string{"config", "sources"}, want: `config: unknown subcommand "sources"`, code: 1},
{name: "missing", args: []string{"config"}, want: "config: expected subcommand: validate|show", code: 1},
{name: "sources help", args: []string{"config", "sources", "--help"}, want: "Usage: narratio config sources"},
{name: "unknown", args: []string{"config", "diff"}, want: `config: unknown subcommand "diff"`, code: 1},
{name: "missing", args: []string{"config"}, want: "config: expected subcommand: validate|show|sources", code: 1},
{name: "session flag", args: []string{"config", "validate", "--session", "session.yml"}, want: "flag provided but not defined", code: 1},
{name: "stage flag", args: []string{"config", "show", "--from", "prepare"}, want: "flag provided but not defined", code: 1},
{name: "force flag", args: []string{"config", "show", "--force"}, want: "flag provided but not defined", code: 1},
@@ -86,6 +87,13 @@ whisperx:
if strings.Contains(show, secret) {
t.Fatalf("show output leaked a raw secret: %q", show)
}
var sourcesOut bytes.Buffer
if err := ConfigSources(context.Background(), []string{"--config", pipelinePath}, &sourcesOut); err != nil {
t.Fatalf("ConfigSources() error = %v", err)
}
if strings.Contains(sourcesOut.String(), secret) {
t.Fatalf("sources output leaked a raw secret: %q", sourcesOut.String())
}
if calls != 0 {
t.Fatalf("inspection invoked configured external endpoint %d time(s)", calls)
}
@@ -189,6 +197,133 @@ whisperx:
}
}
func TestConfigSourcesReportsStableOwnership(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
enableCommandTestProfiles(t, pipelinePath)
args := []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--profile", "testing"}
var first, second bytes.Buffer
if err := ConfigSources(context.Background(), args, &first); err != nil {
t.Fatalf("first ConfigSources() error = %v", err)
}
if err := ConfigSources(context.Background(), args, &second); err != nil {
t.Fatalf("second ConfigSources() error = %v", err)
}
if first.String() != second.String() {
t.Fatalf("sources output is not deterministic:\nfirst=%q\nsecond=%q", first.String(), second.String())
}
output := first.String()
for _, want := range []string{
"root: ",
"imports: none",
"profile: name=testing selection=cli overlay=",
"campaign: id=sample-campaign source=",
"party: mode=legacy source=",
"digest: ",
"path\trole\tsource\n",
"workspace.root\troot\t",
"whisperx.language\tprofile\t",
"campaign.inputs.players_file\tlegacy_player\t",
"trim.enabled\tdefault\tdefault",
} {
if !strings.Contains(output, want) {
t.Fatalf("sources output missing %q:\n%s", want, output)
}
}
}
func TestConfigSourcesReportsCanonicalFamilyAndPublishOrigins(t *testing.T) {
pipelinePath, campaignPath := writeInspectionFamilyConfig(t)
var out bytes.Buffer
if err := ConfigSources(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath}, &out); err != nil {
t.Fatalf("ConfigSources() error = %v", err)
}
output := out.String()
partyPath := filepath.Join(filepath.Dir(campaignPath), "party.yml")
for _, want := range []string{
"party: mode=canonical source=" + partyPath,
"derived.players\tparty\t" + partyPath,
"scriptorium.artifacts.character_note_arannis.enabled\tfamily\t" + pipelinePath,
"scriptorium.artifacts.character_note_arannis.enabled\tparty\t" + partyPath,
"scriptorium.artifacts.character_note_arannis.depends_on\tfamily\t" + pipelinePath,
"scriptorium.artifacts.character_note_arannis.inputs.prior.source\tparty\t" + partyPath,
"publish.outputs[",
"\tfamily\t" + pipelinePath,
"\tparty\t" + partyPath,
} {
if !strings.Contains(output, want) {
t.Fatalf("sources output missing %q:\n%s", want, output)
}
}
}
func TestConfigSourcesPreservesProfileOwnershipThroughFamilyExpansion(t *testing.T) {
pipelinePath, campaignPath := writeInspectionFamilyConfig(t)
data, err := os.ReadFile(pipelinePath)
if err != nil {
t.Fatal(err)
}
profilePath := filepath.Join(filepath.Dir(pipelinePath), "testing.yml")
composition := "composition:\n default_profile: testing\n profiles:\n testing:\n overlay: testing.yml\n"
if err := os.WriteFile(pipelinePath, append([]byte(composition), data...), 0o644); err != nil {
t.Fatal(err)
}
mustWriteTestFile(t, profilePath, `scriptorium:
artifact_families:
character_note:
enabled: true
prompt_id: dnd.character_note
`)
var out bytes.Buffer
if err := ConfigSources(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath}, &out); err != nil {
t.Fatalf("ConfigSources() error = %v", err)
}
want := "scriptorium.artifacts.character_note_arannis.enabled\tfamily\t" + profilePath
if !strings.Contains(out.String(), want) {
t.Fatalf("sources output missing profile-generated ownership %q:\n%s", want, out.String())
}
}
func TestConfigSourcesReportsRootImportProfileAndDefaultOwnership(t *testing.T) {
dir := t.TempDir()
rootPath := filepath.Join(dir, "pipeline.yml")
importPath := filepath.Join(dir, "base.yml")
profilePath := filepath.Join(dir, "testing.yml")
mustWriteTestFile(t, rootPath, `composition:
imports: [base.yml]
default_profile: testing
profiles:
testing:
overlay: testing.yml
workspace:
root: /srv/narratio
whisperx:
transcribe_url: https://transcription.example.com/transcribe
`)
mustWriteTestFile(t, importPath, "storage:\n backend: local\n")
mustWriteTestFile(t, profilePath, "whisperx:\n language: fr\n")
var out bytes.Buffer
if err := ConfigSources(context.Background(), []string{"--config", rootPath}, &out); err != nil {
t.Fatalf("ConfigSources() error = %v", err)
}
output := out.String()
for _, want := range []string{
"import: " + importPath,
"profile: name=testing selection=default overlay=" + profilePath,
"workspace.root\troot\t" + rootPath,
"storage.backend\timport\t" + importPath,
"whisperx.language\tprofile\t" + profilePath,
"trim.enabled\tdefault\tdefault",
} {
if !strings.Contains(output, want) {
t.Fatalf("sources output missing %q:\n%s", want, output)
}
}
}
func writeInspectionFamilyConfig(t *testing.T) (string, string) {
t.Helper()
dir := t.TempDir()
@@ -204,10 +339,25 @@ whisperx:
transcribe_url: https://transcription.example.com/transcribe
scriptorium:
artifact_families:
character_meta:
enabled: false
for_each: party.characters
output_path_pattern: artifacts/characters/{character_id}/meta.md
character_note:
enabled: false
for_each: party.characters
output_path_pattern: artifacts/characters/{character_id}/note.md
member_dependencies: [character_meta]
inputs:
prior: {source: narratio.member_artifact.character_meta, required: true}
member_vars:
character_name: character.name
publish:
enabled: true
required: true
publish:
enabled: true
upload_run: false
`)
mustWriteTestFile(t, campaignPath, `campaign_id: sample-campaign
inputs: