Files
notarius/internal/cli/command_contract_test.go

241 lines
9.4 KiB
Go

package cli
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCommandHelpSpellingsWriteUsageToStdout(t *testing.T) {
tests := [][]string{nil, {"help"}, {"--help"}, {"-h"}}
for _, args := range tests {
name := "no arguments"
if len(args) > 0 {
name = args[0]
}
t.Run(name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
code := RunWithOptions(args, &stdout, &stderr, commandContractOptions(t))
if code != 0 || !strings.Contains(stdout.String(), "Usage:") || stderr.Len() != 0 {
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
})
}
}
func TestCommandSyntaxErrorsUseStderrAndExitTwo(t *testing.T) {
tests := []struct {
name string
args []string
want string
}{
{name: "unknown command", args: []string{"unknown"}, want: "unknown command"},
{name: "missing config subcommand", args: []string{"config"}, want: "config requires a subcommand"},
{name: "unknown pipelines subcommand", args: []string{"pipelines", "unknown"}, want: "unknown pipelines subcommand"},
{name: "malformed run flag", args: []string{"run", "demo", "--chunk_cache", "invalid"}, want: "not supported"},
{name: "unknown flag", args: []string{"config", "validate", "--unknown"}, want: "flag provided but not defined"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
code := RunWithOptions(tt.args, &stdout, &stderr, commandContractOptions(t))
if code != 2 || !strings.Contains(stderr.String(), tt.want) || stdout.Len() != 0 {
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
})
}
}
func TestConfigDiscoveryPrefersExplicitPathThenEnvironment(t *testing.T) {
explicit := writeCommandConfig(t, "explicit", "alpha")
environment := writeCommandConfig(t, "environment", "beta")
lookup := func(name string) (string, bool) {
if name == "NOTARIUS_CONFIG" {
return environment, true
}
return "", false
}
var stdout, stderr bytes.Buffer
code := RunWithOptions([]string{"pipelines", "list", "--config", explicit}, &stdout, &stderr, commandContractOptionsWithLookup(t, lookup))
if code != 0 || stdout.String() != "alpha\nexplicit\n" || stderr.Len() != 0 {
t.Fatalf("explicit config: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
code = RunWithOptions([]string{"pipelines", "list"}, &stdout, &stderr, commandContractOptionsWithLookup(t, lookup))
if code != 0 || stdout.String() != "beta\nenvironment\n" || stderr.Len() != 0 {
t.Fatalf("environment config: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
}
func TestConfigDiscoveryUsesCompiledDefaultOnlyWhenAvailable(t *testing.T) {
info, statErr := os.Stat(defaultConfigPath)
if statErr != nil && !os.IsNotExist(statErr) {
t.Fatalf("stat compiled default config: %v", statErr)
}
if statErr == nil && !info.Mode().IsRegular() {
t.Skipf("compiled default config has unexpected host state: %s", info.Mode())
}
path, err := discoverConfigPath("", commandContractOptions(t))
if statErr == nil {
if err != nil || path != defaultConfigPath {
t.Fatalf("discoverConfigPath() = %q, %v; want compiled default", path, err)
}
return
}
if err == nil || !strings.Contains(err.Error(), "config file not found") {
t.Fatalf("discoverConfigPath() error = %v, want documented not-found context", err)
}
}
func TestConfigLoadingFailuresReturnOneWithPathContext(t *testing.T) {
missing := filepath.Join(t.TempDir(), "missing.yml")
var stdout, stderr bytes.Buffer
code := RunWithOptions([]string{"config", "validate", "--config", missing}, &stdout, &stderr, commandContractOptions(t))
if code != 1 || !strings.Contains(stderr.String(), missing) || stdout.Len() != 0 {
t.Fatalf("missing config: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
malformed := filepath.Join(t.TempDir(), "malformed.yml")
if err := os.WriteFile(malformed, []byte("version: [\n"), 0o600); err != nil {
t.Fatal(err)
}
stdout.Reset()
stderr.Reset()
code = RunWithOptions([]string{"config", "validate", "--config", malformed}, &stdout, &stderr, commandContractOptions(t))
if code != 1 || !strings.Contains(stderr.String(), malformed) || !strings.Contains(stderr.String(), "parse config file") || stdout.Len() != 0 {
t.Fatalf("malformed config: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
}
func TestConfigValidateResolvesPipelineAndChecksSelection(t *testing.T) {
configPath := writeResolvableCommandConfig(t)
options := commandContractOptions(t)
var stdout, stderr bytes.Buffer
code := RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "demo", "--only", "spells"}, &stdout, &stderr, options)
if code != 0 || !strings.Contains(stdout.String(), "valid for pipeline \"demo\"") || stderr.Len() != 0 {
t.Fatalf("valid resolution: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
code = RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "missing"}, &stdout, &stderr, options)
if code != 1 || !strings.Contains(stderr.String(), "pipeline \"missing\"") {
t.Fatalf("unknown pipeline: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
code = RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "demo", "--only", "missing"}, &stdout, &stderr, options)
if code != 1 || !strings.Contains(stderr.String(), "lane \"missing\"") {
t.Fatalf("unknown lane: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
code = RunWithOptions([]string{"config", "validate", "--config", configPath, "--only", "spells"}, &stdout, &stderr, options)
if code != 2 || !strings.Contains(stderr.String(), "--only requires --pipeline") {
t.Fatalf("missing pipeline for only: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
code = RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "demo", "--only", "spells,,other"}, &stdout, &stderr, options)
if code != 2 || !strings.Contains(stderr.String(), "--only must contain") {
t.Fatalf("malformed only: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
}
func TestPipelinesListSortsNormalizedIDsInTextAndJSON(t *testing.T) {
configPath := writeCommandConfig(t, " zeta ", "alpha")
options := commandContractOptions(t)
var stdout, stderr bytes.Buffer
code := RunWithOptions([]string{"pipelines", "list", "--config", configPath}, &stdout, &stderr, options)
if code != 0 || stdout.String() != "alpha\nzeta\n" || stderr.Len() != 0 {
t.Fatalf("text list: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
code = RunWithOptions([]string{"pipelines", "list", "--config", configPath, "--json"}, &stdout, &stderr, options)
var payload struct {
Pipelines []string `json:"pipelines"`
}
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
t.Fatalf("JSON list = %q: %v", stdout.String(), err)
}
if code != 0 || len(payload.Pipelines) != 2 || payload.Pipelines[0] != "alpha" || payload.Pipelines[1] != "zeta" || stderr.Len() != 0 {
t.Fatalf("JSON list: code=%d payload=%#v stderr=%q", code, payload, stderr.String())
}
}
func TestRemovedStructuralFlagsAndRuntimeFailuresKeepExitClasses(t *testing.T) {
configPath := writeResolvableCommandConfig(t)
options := commandContractOptions(t)
var stdout, stderr bytes.Buffer
code := RunWithOptions([]string{"run", "demo", "--input", "missing-input", "--config", configPath, "--diagnostics-dir", t.TempDir()}, &stdout, &stderr, options)
if code != 2 || !strings.Contains(stderr.String(), "flag provided but not defined") {
t.Fatalf("removed flag: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
code = RunWithOptions([]string{"run", "missing", "--input", "missing-input", "--config", configPath, "--chunk_cache", "bypass"}, &stdout, &stderr, options)
if code != 1 || !strings.Contains(stderr.String(), "pipeline \"missing\"") || stdout.Len() != 0 {
t.Fatalf("valid-runtime failure: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
}
func commandContractOptions(t *testing.T) Options {
return commandContractOptionsWithLookup(t, emptyLookup)
}
func commandContractOptionsWithLookup(t *testing.T, lookup func(string) (string, bool)) Options {
t.Helper()
components, err := newProductionComponents()
if err != nil {
t.Fatal(err)
}
return Options{
Catalog: catalogFromRegistries(components.registries),
Registries: components.registries,
LookupEnv: lookup,
}
}
func writeCommandConfig(t *testing.T, firstID, secondID string) string {
t.Helper()
content := fmt.Sprintf("version: 3\npipelines:\n %q:\n input: seriatim\n %q:\n input: seriatim\n", firstID, secondID)
return writeCommandConfigContent(t, content)
}
func writeResolvableCommandConfig(t *testing.T) string {
t.Helper()
return writeCommandConfigContent(t, `version: 3
pipelines:
demo:
input: seriatim
artifacts:
spells:
extract: dnd/spells
`)
}
func writeCommandConfigContent(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "config.yml")
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
return path
}