package diagnostics import ( "bytes" "errors" "io" "os" "os/exec" "path/filepath" "strings" "testing" ) func TestPrepareOutputAllowsInPlaceTranscriptButRejectsReportCollisions(t *testing.T) { dir := t.TempDir() input := filepath.Join(dir, "input.json") if err := os.WriteFile(input, []byte("source"), 0600); err != nil { t.Fatal(err) } alias := filepath.Join(dir, "alias.json") if err := os.Symlink(input, alias); err != nil { t.Fatal(err) } hardlink := filepath.Join(dir, "hardlink.json") if err := os.Link(input, hardlink); err != nil { t.Fatal(err) } for _, output := range []string{input, alias, hardlink} { if _, err := PrepareOutput(OutputSelection{TranscriptPath: output, InputTranscriptPath: input}); err != nil { t.Fatalf("in-place output rejected: %s: %v", output, err) } } if _, err := PrepareOutput(OutputSelection{TranscriptPath: filepath.Join(dir, ErrorsFile), ReportDir: dir, ReportingEnabled: true}); err == nil { t.Fatal("report/transcript collision accepted") } if _, err := PrepareOutput(OutputSelection{ReportDir: dir, ReportingEnabled: true, ProtectedPaths: []string{filepath.Join(dir, ErrorsFile)}}); err == nil { t.Fatal("report/source collision accepted") } if err := os.Symlink(input, filepath.Join(dir, ErrorsFile)); err != nil { t.Fatal(err) } if _, err := PrepareOutput(OutputSelection{ReportDir: dir, ReportingEnabled: true, InputTranscriptPath: input}); err == nil { t.Fatal("report/input alias accepted") } if err := os.Remove(filepath.Join(dir, ErrorsFile)); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(dir, MarkdownFile), 0700); err != nil { t.Fatal(err) } if _, err := PrepareOutput(OutputSelection{ReportDir: dir, ReportingEnabled: true}); err == nil { t.Fatal("directory report target accepted") } if got, err := os.ReadFile(input); err != nil || string(got) != "source" { t.Fatalf("source changed: %s %v", got, err) } } func TestPrepareOutputPreservesExistingTranscriptAndChecksParent(t *testing.T) { dir := t.TempDir() output := filepath.Join(dir, "out.json") if err := os.WriteFile(output, []byte("old content"), 0600); err != nil { t.Fatal(err) } session, err := PrepareOutput(OutputSelection{TranscriptPath: output}) if err != nil { t.Fatal(err) } if got, err := os.ReadFile(output); err != nil || string(got) != "old content" { t.Fatalf("preflight truncated output: %s %v", got, err) } if failures := session.Finalize([]byte("new content"), ReportBundle{}, func([]FileFailure) ReportBundle { return ReportBundle{} }); len(failures) != 0 { t.Fatal(failures) } if got, err := os.ReadFile(output); err != nil || string(got) != "new content" { t.Fatalf("publication: %s %v", got, err) } if info, _ := os.Stat(output); info.Mode().Perm() != 0600 { t.Fatalf("permission changed: %v", info.Mode()) } if _, err := PrepareOutput(OutputSelection{TranscriptPath: filepath.Join(dir, "missing", "out.json")}); err == nil { t.Fatal("absent parent accepted") } if _, err := PrepareOutput(OutputSelection{TranscriptPath: dir}); err == nil { t.Fatal("directory target accepted") } entries, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } for _, entry := range entries { if strings.HasPrefix(entry.Name(), ".audita-") { t.Fatalf("probe left behind: %s", entry.Name()) } } } func TestAtomicReplacementCleansTemporaryFileAfterRenameFailure(t *testing.T) { dir := t.TempDir() blocked := filepath.Join(dir, "directory-target") if err := os.Mkdir(blocked, 0700); err != nil { t.Fatal(err) } if err := atomicReplace(blocked, []byte("new content")); err == nil { t.Fatal("directory replacement unexpectedly succeeded") } entries, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } for _, entry := range entries { if strings.HasPrefix(entry.Name(), ".audita-write-") { t.Fatalf("replacement left temporary file: %s", entry.Name()) } } } func TestDisabledReportingIgnoresUnusableDirectory(t *testing.T) { dir := t.TempDir() session, err := PrepareOutput(OutputSelection{TranscriptPath: filepath.Join(dir, "out.json"), ReportDir: filepath.Join(dir, "missing"), ReportingEnabled: false}) if err != nil || len(session.ReportPaths()) != 0 { t.Fatalf("disabled reports probed: %+v %v", session, err) } if err := session.Initialize(ReportBundle{}, ReportBundle{}); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dir, "missing")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("disabled destination created: %v", err) } } func TestEnabledReportingRequiresExistingDirectory(t *testing.T) { dir := t.TempDir() if _, err := PrepareOutput(OutputSelection{ReportDir: filepath.Join(dir, "missing"), ReportingEnabled: true}); err == nil { t.Fatal("missing report directory accepted") } if _, err := os.Stat(filepath.Join(dir, "missing")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("report parent created: %v", err) } } func TestStdoutModeInitializesAndFinalizesReports(t *testing.T) { dir := t.TempDir() session, err := PrepareOutput(OutputSelection{ReportDir: dir, ReportingEnabled: true}) if err != nil { t.Fatal(err) } initial := ReportBundle{Errors: []byte("in progress errors"), Recoveries: []byte("in progress recoveries"), Markdown: []byte("in progress report")} finished := ReportBundle{Errors: []byte("completed errors"), Recoveries: []byte("completed recoveries"), Markdown: []byte("completed report")} if err := session.Initialize(initial, ReportBundle{}); err != nil { t.Fatal(err) } if session.TranscriptPath() != "" { t.Fatal("stdout mode acquired transcript path") } for i, path := range session.ReportPaths() { got, err := os.ReadFile(path) if err != nil || string(got) != string(bundleParts(initial)[i]) { t.Fatalf("initial %s: %s %v", path, got, err) } } if failures := session.Finalize(nil, finished, func([]FileFailure) ReportBundle { return ReportBundle{} }); len(failures) != 0 { t.Fatal(failures) } for i, path := range session.ReportPaths() { got, err := os.ReadFile(path) if err != nil || string(got) != string(bundleParts(finished)[i]) { t.Fatalf("final %s: %s %v", path, got, err) } } } func TestInitializeFailureMarksPriorReportFailedBeforeModelWork(t *testing.T) { dir := t.TempDir() session, err := PrepareOutput(OutputSelection{ReportDir: dir, ReportingEnabled: true}) if err != nil { t.Fatal(err) } actual := session.replace session.replace = func(path string, payload []byte) error { if filepath.Base(path) == RecoveriesFile && string(payload) == "initial" { return os.ErrPermission } return actual(path, payload) } modelCalled := false if err := session.Initialize(ReportBundle{Errors: []byte("initial"), Recoveries: []byte("initial"), Markdown: []byte("initial")}, ReportBundle{Errors: []byte("failed"), Recoveries: []byte("failed"), Markdown: []byte("failed")}); err == nil { modelCalled = true } if modelCalled { t.Fatal("model work continued after initialization failure") } got, err := os.ReadFile(filepath.Join(dir, ErrorsFile)) if err != nil || string(got) != "failed" { t.Fatalf("prior report not failed: %s %v", got, err) } if got, err := os.ReadFile(filepath.Join(dir, MarkdownFile)); err != nil || string(got) != "failed" { t.Fatalf("later report not finalized: %q %v", got, err) } } func TestFinalizationCollectsFailuresAndRetainsPublishedTranscript(t *testing.T) { dir := t.TempDir() output := filepath.Join(dir, "output.json") session, err := PrepareOutput(OutputSelection{TranscriptPath: output, ReportDir: dir, ReportingEnabled: true}) if err != nil { t.Fatal(err) } if err := session.Initialize(ReportBundle{Errors: []byte("initial"), Recoveries: []byte("initial"), Markdown: []byte("initial")}, ReportBundle{}); err != nil { t.Fatal(err) } actual := session.replace session.replace = func(path string, payload []byte) error { if filepath.Base(path) == ErrorsFile && string(payload) == "complete" { return os.ErrPermission } return actual(path, payload) } complete := ReportBundle{Errors: []byte("complete"), Recoveries: []byte("complete"), Markdown: []byte("complete")} failed := ReportBundle{Errors: []byte("failed"), Recoveries: []byte("failed"), Markdown: []byte("failed")} failures := session.Finalize([]byte("transcript"), complete, func([]FileFailure) ReportBundle { return failed }) if len(failures) != 1 || failures[0].Path != filepath.Join(dir, ErrorsFile) { t.Fatalf("failures: %+v", failures) } if got, err := os.ReadFile(output); err != nil || string(got) != "transcript" { t.Fatalf("transcript erased: %s %v", got, err) } for _, path := range session.ReportPaths() { if got, err := os.ReadFile(path); err != nil || string(got) != "failed" { t.Fatalf("failed-status pass %s: %s %v", path, got, err) } } } func TestDebugRunDirectoryUsesSuppliedIDAndIsRetained(t *testing.T) { parent := filepath.Join(t.TempDir(), "debug") if _, err := os.Stat(parent); !errors.Is(err, os.ErrNotExist) { t.Fatalf("unexpected parent: %v", err) } run, err := NewRunDirectory(parent, "run-123", "secret") if err != nil { t.Fatal(err) } if run.Path() != filepath.Join(parent, "run-123") { t.Fatalf("run ID changed: %s", run.Path()) } if _, err := NewRunDirectory(parent, "run-123"); err == nil { t.Fatal("duplicate run ID accepted") } if err := run.WriteJSONArtifact(ArtifactCorrectionLedger, map[string]string{"proposal": "secret"}); err != nil { t.Fatal(err) } contents, err := os.ReadFile(filepath.Join(run.Path(), ArtifactCorrectionLedger)) if err != nil || strings.Contains(string(contents), "secret") { t.Fatalf("debug artifact: %s %v", contents, err) } if _, err := os.Stat(run.Path()); err != nil { t.Fatalf("debug directory removed: %v", err) } } func TestSelectionAndAbortPreserveTranscript(t *testing.T) { dir := t.TempDir() output := filepath.Join(dir, "output.json") if err := os.WriteFile(output, []byte("original"), 0600); err != nil { t.Fatal(err) } session, err := SelectOutput(OutputSelection{TranscriptPath: output}) if err != nil { t.Fatal(err) } if entries, err := os.ReadDir(dir); err != nil || len(entries) != 1 { t.Fatalf("selection changed directory: %v %v", entries, err) } if err := session.Prepare(); err != nil { t.Fatal(err) } if session.temp == nil { t.Fatal("temporary output not open") } temp := session.tempPath if err := session.Close(); err != nil { t.Fatal(err) } if _, err := os.Stat(temp); !errors.Is(err, os.ErrNotExist) { t.Fatalf("unpublished file remains: %v", err) } if data, err := os.ReadFile(output); err != nil || string(data) != "original" { t.Fatalf("output changed: %q %v", data, err) } } func TestStdoutAndDeviceDelivery(t *testing.T) { var stdout bytes.Buffer session, err := PrepareOutput(OutputSelection{Stdout: &stdout}) if err != nil { t.Fatal(err) } if failures := session.Finalize([]byte("transcript"), ReportBundle{}, nil); len(failures) != 0 { t.Fatal(failures) } if stdout.String() != "transcript" { t.Fatalf("stdout=%q", stdout.String()) } session, err = PrepareOutput(OutputSelection{TranscriptPath: os.DevNull}) if err != nil { t.Fatal(err) } if !session.direct || session.temp != nil { t.Fatal("device was not deferred") } if failures := session.Finalize([]byte("discard"), ReportBundle{}, nil); len(failures) != 0 { t.Fatal(failures) } } func TestFinishRetainsPrimaryFailureAndAttemptsReports(t *testing.T) { dir := t.TempDir() output := filepath.Join(dir, "transcript.json") if err := os.WriteFile(output, []byte("original"), 0600); err != nil { t.Fatal(err) } session, err := PrepareOutput(OutputSelection{TranscriptPath: output, ReportDir: dir, ReportingEnabled: true}) if err != nil { t.Fatal(err) } originalReplace := session.replace session.replace = func(path string, data []byte) error { if filepath.Base(path) == ErrorsFile { return os.ErrPermission } return originalReplace(path, data) } primary := errors.New("processing failed") bundle := ReportBundle{Errors: []byte("failed"), Recoveries: []byte("failed"), Markdown: []byte("failed")} result := session.Finish(primary, []byte("unwanted"), bundle, func([]FileFailure) ReportBundle { return bundle }) if !errors.Is(result, primary) || !errors.Is(result, os.ErrPermission) { t.Fatalf("combined error: %v", result) } if data, err := os.ReadFile(output); err != nil || string(data) != "original" { t.Fatalf("output=%q err=%v", data, err) } if data, err := os.ReadFile(filepath.Join(dir, MarkdownFile)); err != nil || string(data) != "failed" { t.Fatalf("remaining report=%q err=%v", data, err) } } func TestOptionalDebugPreparationReportsProblems(t *testing.T) { dir := t.TempDir() session, err := SelectOutput(OutputSelection{}) if err != nil { t.Fatal(err) } file := filepath.Join(dir, "not-directory") if err := os.WriteFile(file, []byte("x"), 0600); err != nil { t.Fatal(err) } if run, problems := session.PrepareDebug(file, "run", InvocationMetadata{}, nil); run != nil || len(problems) == 0 { t.Fatalf("run=%v problems=%v", run, problems) } run, problems := session.PrepareDebug(filepath.Join(dir, "debug"), "run", InvocationMetadata{Operation: "process"}, []byte("secret: sensitive\n"), "sensitive") if len(problems) != 0 { t.Fatal(problems) } contents, err := os.ReadFile(filepath.Join(run.Path(), ArtifactEffectiveConfig)) if err != nil { t.Fatal(err) } if strings.Contains(string(contents), "sensitive") { t.Fatal("debug artifact contains secret") } } type shortOutput struct{} func (shortOutput) Write(b []byte) (int, error) { return len(b) / 2, nil } func TestShortStdoutWriteIsFailure(t *testing.T) { session, err := PrepareOutput(OutputSelection{Stdout: shortOutput{}}) if err != nil { t.Fatal(err) } failures := session.Finalize([]byte("transcript"), ReportBundle{}, nil) if len(failures) != 1 || !errors.Is(failures[0], io.ErrShortWrite) { t.Fatalf("failures=%v", failures) } } func TestTranscriptWriteFailureCleansTemporaryResource(t *testing.T) { dir := t.TempDir() output := filepath.Join(dir, "out.json") if err := os.WriteFile(output, []byte("original"), 0600); err != nil { t.Fatal(err) } session, err := PrepareOutput(OutputSelection{TranscriptPath: output}) if err != nil { t.Fatal(err) } temp := session.tempPath if err := session.temp.Close(); err != nil { t.Fatal(err) } failures := session.Finalize([]byte("new"), ReportBundle{}, nil) if len(failures) == 0 { t.Fatal("closed output accepted") } if _, err := os.Stat(temp); !errors.Is(err, os.ErrNotExist) { t.Fatalf("temporary output remains: %v", err) } if data, err := os.ReadFile(output); err != nil || string(data) != "original" { t.Fatalf("output=%q err=%v", data, err) } } func TestNamedPipeIsNotOpenedDuringPreparation(t *testing.T) { tool, err := exec.LookPath("mkfifo") if err != nil { t.Skip("mkfifo is unavailable") } pipe := filepath.Join(t.TempDir(), "output-pipe") if output, err := exec.Command(tool, pipe).CombinedOutput(); err != nil { t.Fatalf("mkfifo: %v %s", err, output) } session, err := PrepareOutput(OutputSelection{TranscriptPath: pipe}) if err != nil { t.Fatal(err) } if !session.direct || session.temp != nil { t.Fatal("pipe was not deferred") } if err := session.Close(); err != nil { t.Fatal(err) } }