Add read-only configuration inspection commands
This commit is contained in:
226
internal/app/config_commands_test.go
Normal file
226
internal/app/config_commands_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
)
|
||||
|
||||
func TestConfigCommandHelpAndDispatch(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
code int
|
||||
}{
|
||||
{name: "top level help", args: []string{"config", "--help"}, want: "Usage: narratio config <validate|show>"},
|
||||
{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: "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},
|
||||
{name: "artifact flag", args: []string{"config", "show", "--artifacts", "recap"}, want: "flag provided but not defined", code: 1},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := Execute(test.args, &stdout, &stderr)
|
||||
if code != test.code {
|
||||
t.Fatalf("exit code = %d, want %d; stdout=%q stderr=%q", code, test.code, stdout.String(), stderr.String())
|
||||
}
|
||||
combined := stdout.String() + stderr.String()
|
||||
if !strings.Contains(combined, test.want) {
|
||||
t.Fatalf("output = %q, want %q", combined, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidateAndShowPipelineOnlyAreSideEffectFree(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
workspaceRoot := filepath.Join(dir, "workspace-does-not-exist")
|
||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||
secretsDir := filepath.Join(dir, "secrets")
|
||||
const secret = "inspection-secret-must-not-escape"
|
||||
mustWriteTestFile(t, filepath.Join(secretsDir, "INSPECTION_SECRET"), secret+"\n")
|
||||
calls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { calls++ }))
|
||||
defer server.Close()
|
||||
mustWriteTestFile(t, pipelinePath, `workspace:
|
||||
root: `+workspaceRoot+`
|
||||
secrets:
|
||||
env_dir: `+secretsDir+`
|
||||
whisperx:
|
||||
transcribe_url: `+server.URL+`
|
||||
`)
|
||||
t.Setenv("INSPECTION_SECRET", secret)
|
||||
|
||||
var validateOut bytes.Buffer
|
||||
if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath}, &validateOut); err != nil {
|
||||
t.Fatalf("ConfigValidate() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(validateOut.String(), "Configuration valid: root=") || !strings.Contains(validateOut.String(), "; profile=none; digest=") {
|
||||
t.Fatalf("validate output = %q", validateOut.String())
|
||||
}
|
||||
|
||||
var showOut bytes.Buffer
|
||||
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath}, &showOut); err != nil {
|
||||
t.Fatalf("ConfigShow() error = %v", err)
|
||||
}
|
||||
show := showOut.String()
|
||||
if !strings.HasSuffix(show, "\n") || !strings.Contains(show, "workspace:\n") || !strings.Contains(show, "root: "+workspaceRoot) {
|
||||
t.Fatalf("show output = %q", show)
|
||||
}
|
||||
if strings.Contains(show, "artifact_families") || strings.Contains(show, "resolution") {
|
||||
t.Fatalf("show output leaked resolution-only fields: %q", show)
|
||||
}
|
||||
if strings.Contains(show, secret) {
|
||||
t.Fatalf("show output leaked a raw secret: %q", show)
|
||||
}
|
||||
if calls != 0 {
|
||||
t.Fatalf("inspection invoked configured external endpoint %d time(s)", calls)
|
||||
}
|
||||
if _, err := os.Stat(workspaceRoot); !os.IsNotExist(err) {
|
||||
t.Fatalf("workspace root stat error = %v, want absent", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigCommandsSelectProfilesAndCampaigns(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
|
||||
enableCommandTestProfiles(t, pipelinePath)
|
||||
|
||||
var defaultOut bytes.Buffer
|
||||
if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath}, &defaultOut); err != nil {
|
||||
t.Fatalf("default ConfigValidate() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(defaultOut.String(), "profile=production") {
|
||||
t.Fatalf("default output = %q", defaultOut.String())
|
||||
}
|
||||
|
||||
var explicitOut bytes.Buffer
|
||||
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath, "--profile", "testing"}, &explicitOut); err != nil {
|
||||
t.Fatalf("explicit ConfigShow() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(explicitOut.String(), "language: fr") {
|
||||
t.Fatalf("explicit show output = %q", explicitOut.String())
|
||||
}
|
||||
|
||||
for _, args := range [][]string{
|
||||
{"--config", pipelinePath, "--campaign", "sample-campaign"},
|
||||
{"--config", pipelinePath, "--campaign-file", campaignPath},
|
||||
} {
|
||||
var out bytes.Buffer
|
||||
if err := ConfigValidate(context.Background(), args, &out); err != nil {
|
||||
t.Fatalf("ConfigValidate(%q) error = %v", args, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigCommandsExpandFamiliesAndRequireCampaign(t *testing.T) {
|
||||
pipelinePath, campaignPath := writeInspectionFamilyConfig(t)
|
||||
|
||||
for _, command := range []func(context.Context, []string, io.Writer) error{ConfigValidate, ConfigShow} {
|
||||
var out bytes.Buffer
|
||||
err := command(context.Background(), []string{"--config", pipelinePath}, &out)
|
||||
if err == nil || !strings.Contains(err.Error(), "artifact_families requires a campaign") {
|
||||
t.Fatalf("family command without campaign error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var show bytes.Buffer
|
||||
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath}, &show); err != nil {
|
||||
t.Fatalf("ConfigShow() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(show.String(), "character_note_arannis:") || strings.Contains(show.String(), "artifact_families") {
|
||||
t.Fatalf("expanded show output = %q", show.String())
|
||||
}
|
||||
|
||||
loadedPipeline, err := config.LoadPipeline(pipelinePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loadedCampaign, err := config.LoadCampaign(campaignPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err := config.LoadPipelineCampaign(pipelinePath, loadedPipeline, campaignPath, loadedCampaign)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var validateOut bytes.Buffer
|
||||
if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath}, &validateOut); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(validateOut.String(), "digest="+config.EffectivePipelineDigest(loaded.Pipeline)) {
|
||||
t.Fatalf("inspection digest = %q, want %q", validateOut.String(), config.EffectivePipelineDigest(loaded.Pipeline))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidateReportsCanonicalPartyAndImportErrors(t *testing.T) {
|
||||
pipelinePath, campaignPath := writeInspectionFamilyConfig(t)
|
||||
mustWriteTestFile(t, filepath.Join(filepath.Dir(campaignPath), "party.yml"), "schema_version: narratio.party.v2\ncharacters: {}\n")
|
||||
if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath}, io.Discard); err == nil || !strings.Contains(err.Error(), "unsupported") {
|
||||
t.Fatalf("canonical party error = %v", err)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
rootPath := filepath.Join(dir, "pipeline.yml")
|
||||
mustWriteTestFile(t, rootPath, `composition:
|
||||
imports: [conf.yml]
|
||||
workspace:
|
||||
root: /one
|
||||
whisperx:
|
||||
transcribe_url: https://transcription.example.com/transcribe
|
||||
`)
|
||||
mustWriteTestFile(t, filepath.Join(dir, "conf.yml"), "workspace:\n root: /two\n")
|
||||
err := ConfigValidate(context.Background(), []string{"--config", rootPath}, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "workspace.root") || !strings.Contains(err.Error(), "pipeline.yml") || !strings.Contains(err.Error(), "conf.yml") {
|
||||
t.Fatalf("import diagnostic = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeInspectionFamilyConfig(t *testing.T) (string, string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
campaignRoot := filepath.Join(dir, "campaigns")
|
||||
campaignDir := filepath.Join(campaignRoot, "sample-campaign")
|
||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||
campaignPath := filepath.Join(campaignDir, "campaign.yml")
|
||||
mustWriteTestFile(t, pipelinePath, `workspace:
|
||||
root: `+filepath.Join(dir, "workspace")+`
|
||||
campaigns:
|
||||
root: `+campaignRoot+`
|
||||
whisperx:
|
||||
transcribe_url: https://transcription.example.com/transcribe
|
||||
scriptorium:
|
||||
artifact_families:
|
||||
character_note:
|
||||
enabled: false
|
||||
for_each: party.characters
|
||||
output_path_pattern: artifacts/characters/{character_id}/note.md
|
||||
`)
|
||||
mustWriteTestFile(t, campaignPath, `campaign_id: sample-campaign
|
||||
inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
party_file: ./party.yml
|
||||
`)
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "party.yml"), `schema_version: narratio.party.v1
|
||||
characters:
|
||||
arannis:
|
||||
player: {name: Eric}
|
||||
character: {name: Arannis, classes: [{name: wizard}]}
|
||||
`)
|
||||
return pipelinePath, campaignPath
|
||||
}
|
||||
Reference in New Issue
Block a user