package cli import ( "bytes" "os" "path/filepath" "strings" "testing" "time" ) const stateSurfaceRunID = "run-1000000000-55555555555555555555555555555555" func stateSurfaceRunIDGenerator(time.Time) (string, error) { return stateSurfaceRunID, nil } func TestRunRejectsDebugDirectoryWithoutDebug(t *testing.T) { var stdout, stderr bytes.Buffer code := RunWithOptions([]string{"run", "example", "--input", "source.json", "--debug-dir", t.TempDir()}, &stdout, &stderr, Options{}) if code != 2 || !strings.Contains(stderr.String(), "--debug-dir requires --debug") { t.Fatalf("code=%d stderr=%q", code, stderr.String()) } } func TestRunDebugAllocatesBeforePipelineResolution(t *testing.T) { root := t.TempDir() configPath := writeV3Config(t, "") var stdout, stderr bytes.Buffer code := RunWithOptions([]string{"run", "missing", "--config", configPath, "--input", "source.json", "--debug", "--debug-dir", root, "--chunk_cache", "bypass"}, &stdout, &stderr, Options{LookupEnv: emptyLookup, RunIDGenerator: stateSurfaceRunIDGenerator}) if code != 1 { t.Fatalf("code=%d stderr=%q", code, stderr.String()) } entries, err := os.ReadDir(root) if err != nil || len(entries) != 1 { t.Fatalf("debug bundles: %v, %v", entries, err) } if entries[0].Name() != stateSurfaceRunID { t.Fatalf("debug bundle name = %q, want %q", entries[0].Name(), stateSurfaceRunID) } bundle := filepath.Join(root, stateSurfaceRunID) for _, name := range []string{"summary", "trace"} { if info, err := os.Stat(filepath.Join(bundle, name)); err != nil || !info.IsDir() { t.Fatalf("%s: %v", name, err) } } if !strings.Contains(stderr.String(), "debug=") { t.Fatalf("stderr does not include bundle path: %q", stderr.String()) } } func TestRunWithoutDebugDoesNotAllocateDebugRoot(t *testing.T) { root := filepath.Join(t.TempDir(), "not-created") configPath := writeV3Config(t, "") var stdout, stderr bytes.Buffer lookup := func(name string) (string, bool) { if name == "NOTARIUS_DEBUG_DIR" { return root, true } return "", false } code := RunWithOptions([]string{"run", "missing", "--config", configPath, "--input", "source.json", "--chunk_cache", "bypass"}, &stdout, &stderr, Options{LookupEnv: lookup, RunIDGenerator: stateSurfaceRunIDGenerator}) if code != 1 { t.Fatalf("code=%d stderr=%q", code, stderr.String()) } if _, err := os.Stat(root); !os.IsNotExist(err) { t.Fatalf("debug root exists or unexpected error: %v", err) } } func TestConfigValidateUsesVersion3AndRemovedFieldsFail(t *testing.T) { configPath := writeV3Config(t, "") var stdout, stderr bytes.Buffer if code := RunWithOptions([]string{"config", "validate", "--config", configPath}, &stdout, &stderr, Options{LookupEnv: emptyLookup}); code != 0 { t.Fatalf("code=%d stderr=%q", code, stderr.String()) } legacy := filepath.Join(t.TempDir(), "legacy.yml") if err := os.WriteFile(legacy, []byte("version: 3\nworkspace:\n directory: /tmp/old\n"), 0o600); err != nil { t.Fatal(err) } stdout.Reset() stderr.Reset() if code := RunWithOptions([]string{"config", "validate", "--config", legacy}, &stdout, &stderr, Options{LookupEnv: emptyLookup}); code != 1 || !strings.Contains(stderr.String(), "field workspace not found") { t.Fatalf("code=%d stderr=%q", code, stderr.String()) } } func writeV3Config(t *testing.T, extra string) string { t.Helper() path := filepath.Join(t.TempDir(), "config.yml") data := "version: 3\noutput:\n directory: ./out\ncache:\n chunk_plans:\n mode: bypass\n checkpoints: {}\ndebug:\n directory: ./debug\n" + extra + "pipelines: {}\n" if err := os.WriteFile(path, []byte(data), 0o600); err != nil { t.Fatal(err) } return path } func emptyLookup(string) (string, bool) { return "", false }