Add semantic configuration profile comparison
This commit is contained in:
@@ -21,12 +21,13 @@ func TestConfigCommandHelpAndDispatch(t *testing.T) {
|
||||
want string
|
||||
code int
|
||||
}{
|
||||
{name: "top level help", args: []string{"config", "--help"}, want: "Usage: narratio config <validate|show|sources>"},
|
||||
{name: "top level help", args: []string{"config", "--help"}, want: "Usage: narratio config <validate|show|sources|diff>"},
|
||||
{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: "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: "diff help", args: []string{"config", "diff", "--help"}, want: "Usage: narratio config diff"},
|
||||
{name: "unknown", args: []string{"config", "unknown"}, want: `config: unknown subcommand "unknown"`, code: 1},
|
||||
{name: "missing", args: []string{"config"}, want: "config: expected subcommand: validate|show|sources|diff", 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},
|
||||
@@ -94,6 +95,14 @@ whisperx:
|
||||
if strings.Contains(sourcesOut.String(), secret) {
|
||||
t.Fatalf("sources output leaked a raw secret: %q", sourcesOut.String())
|
||||
}
|
||||
writeInspectionProfiles(t, pipelinePath, "whisperx:\n language: en\n", "whisperx:\n language: fr\n")
|
||||
var diffOut bytes.Buffer
|
||||
if err := ConfigDiff(context.Background(), []string{"production", "testing", "--config", pipelinePath}, &diffOut); err != nil {
|
||||
t.Fatalf("ConfigDiff() error = %v", err)
|
||||
}
|
||||
if strings.Contains(diffOut.String(), secret) {
|
||||
t.Fatalf("diff output leaked a raw secret: %q", diffOut.String())
|
||||
}
|
||||
if calls != 0 {
|
||||
t.Fatalf("inspection invoked configured external endpoint %d time(s)", calls)
|
||||
}
|
||||
@@ -102,6 +111,152 @@ whisperx:
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigDiffReportsSemanticProfileChanges(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, _, _ := writeValidConfigFiles(t, workspaceRoot)
|
||||
writeInspectionProfiles(t, pipelinePath, `workspace:
|
||||
cleanup_after_publish: true
|
||||
whisperx:
|
||||
language: en
|
||||
audita:
|
||||
modules: [glossary, grammar]
|
||||
scriptorium:
|
||||
artifacts:
|
||||
production_only:
|
||||
enabled: false
|
||||
output_path: artifacts/production.md
|
||||
`, `workspace:
|
||||
cleanup_after_publish: false
|
||||
whisperx:
|
||||
language: fr
|
||||
audita:
|
||||
modules: []
|
||||
scriptorium:
|
||||
artifacts:
|
||||
testing_only:
|
||||
enabled: false
|
||||
output_path: artifacts/testing.md
|
||||
`)
|
||||
|
||||
args := []string{"production", "testing", "--config", pipelinePath}
|
||||
var first, second bytes.Buffer
|
||||
if err := ConfigDiff(context.Background(), args, &first); err != nil {
|
||||
t.Fatalf("ConfigDiff() error = %v", err)
|
||||
}
|
||||
if err := ConfigDiff(context.Background(), args, &second); err != nil {
|
||||
t.Fatalf("second ConfigDiff() error = %v", err)
|
||||
}
|
||||
if first.String() != second.String() {
|
||||
t.Fatalf("diff output is not deterministic:\nfirst=%q\nsecond=%q", first.String(), second.String())
|
||||
}
|
||||
output := first.String()
|
||||
for _, want := range []string{
|
||||
"changed\taudita.modules\t[\"glossary\",\"grammar\"]\t[]\n",
|
||||
"changed\tworkspace.cleanup_after_publish\ttrue\tfalse\n",
|
||||
"changed\twhisperx.language\t\"en\"\t\"fr\"\n",
|
||||
"removed\tscriptorium.artifacts.production_only.enabled\tfalse\n",
|
||||
"added\tscriptorium.artifacts.testing_only.enabled\tfalse\n",
|
||||
} {
|
||||
if !strings.Contains(output, want) {
|
||||
t.Fatalf("diff output missing %q:\n%s", want, output)
|
||||
}
|
||||
}
|
||||
if strings.Index(output, "audita.modules") > strings.Index(output, "workspace.cleanup_after_publish") {
|
||||
t.Fatalf("diff records are not sorted by path:\n%s", output)
|
||||
}
|
||||
var reversed bytes.Buffer
|
||||
if err := ConfigDiff(context.Background(), []string{"testing", "production", "--config", pipelinePath}, &reversed); err != nil {
|
||||
t.Fatalf("reversed ConfigDiff() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(reversed.String(), "changed\twhisperx.language\t\"fr\"\t\"en\"\n") {
|
||||
t.Fatalf("reversed diff did not independently resolve profiles:\n%s", reversed.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigDiffComparesExpandedFamiliesAndPublishRules(t *testing.T) {
|
||||
pipelinePath, campaignPath := writeInspectionFamilyConfig(t)
|
||||
writeInspectionProfiles(t, pipelinePath, `scriptorium:
|
||||
artifact_families:
|
||||
character_note:
|
||||
enabled: false
|
||||
member_vars:
|
||||
character_name: character.name
|
||||
publish:
|
||||
enabled: true
|
||||
required: false
|
||||
`, `scriptorium:
|
||||
artifact_families:
|
||||
character_note:
|
||||
enabled: true
|
||||
prompt_id: dnd.character_note
|
||||
member_vars:
|
||||
character_name: character.class_summary
|
||||
publish:
|
||||
enabled: true
|
||||
required: true
|
||||
`)
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := ConfigDiff(context.Background(), []string{
|
||||
"production", "testing", "--config", pipelinePath, "--campaign-file", campaignPath,
|
||||
}, &out); err != nil {
|
||||
t.Fatalf("ConfigDiff() error = %v", err)
|
||||
}
|
||||
output := out.String()
|
||||
for _, want := range []string{
|
||||
"changed\tscriptorium.artifacts.character_note_arannis.enabled\tfalse\ttrue\n",
|
||||
"changed\tscriptorium.artifacts.character_note_arannis.vars.character_name\t\"Arannis\"\t\"wizard\"\n",
|
||||
"changed\tpublish.outputs\t",
|
||||
} {
|
||||
if !strings.Contains(output, want) {
|
||||
t.Fatalf("family diff output missing %q:\n%s", want, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigDiffReportsEqualityAndRejectsInvalidInput(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
|
||||
writeInspectionProfiles(t, pipelinePath, "whisperx:\n language: en\n", "whisperx:\n language: en\n")
|
||||
|
||||
var equal bytes.Buffer
|
||||
if err := ConfigDiff(context.Background(), []string{"production", "testing", "--config", pipelinePath}, &equal); err != nil {
|
||||
t.Fatalf("ConfigDiff() equal profiles error = %v", err)
|
||||
}
|
||||
if got := equal.String(); got != "no differences\n" {
|
||||
t.Fatalf("equal diff output = %q", got)
|
||||
}
|
||||
|
||||
for _, args := range [][]string{
|
||||
{"production", "testing", "extra", "--config", pipelinePath},
|
||||
{"production", "--config", pipelinePath},
|
||||
{"production", "production", "--config", pipelinePath},
|
||||
{"unknown", "testing", "--config", pipelinePath},
|
||||
{"production", "testing", "--config", pipelinePath, "--profile", "production"},
|
||||
{"production", "testing", "--config", pipelinePath, "--config", pipelinePath},
|
||||
{"production", "testing", "--config", pipelinePath, "--campaign", "sample-campaign", "--campaign-file", campaignPath},
|
||||
} {
|
||||
if err := ConfigDiff(context.Background(), args, io.Discard); err == nil {
|
||||
t.Fatalf("ConfigDiff(%q) succeeded, want error", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeInspectionProfiles(t *testing.T, pipelinePath, production, testing string) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(pipelinePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
composition := "composition:\n default_profile: production\n profiles:\n production:\n overlay: production.yml\n testing:\n overlay: testing.yml\n"
|
||||
if err := os.WriteFile(pipelinePath, append([]byte(composition), data...), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := filepath.Dir(pipelinePath)
|
||||
mustWriteTestFile(t, filepath.Join(dir, "production.yml"), production)
|
||||
mustWriteTestFile(t, filepath.Join(dir, "testing.yml"), testing)
|
||||
}
|
||||
|
||||
func TestConfigCommandsSelectProfilesAndCampaigns(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
|
||||
|
||||
Reference in New Issue
Block a user