package diagnostics import ( "encoding/json" "os" "path/filepath" "regexp" "strings" "testing" "time" "gitea.maximumdirect.net/eric/notarius/internal/core/artifacts" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) func TestNewRunDirectoryCreatesRunDirectoryAndRunID(t *testing.T) { workDir := t.TempDir() runDir, err := NewRunDirectory(workDir, RetentionAuto) if err != nil { t.Fatalf("NewRunDirectory: %v", err) } if filepath.Dir(runDir.Path()) != workDir { t.Fatalf("unexpected run directory parent: %q", runDir.Path()) } if ok := regexp.MustCompile(`^run-\d+$`).MatchString(runDir.RunID()); !ok { t.Fatalf("unexpected run ID: %q", runDir.RunID()) } info, err := os.Stat(runDir.Path()) if err != nil { t.Fatalf("stat run directory: %v", err) } if !info.IsDir() { t.Fatalf("expected run path to be a directory") } } func TestNewRunDirectoryUsesDefaultWorkDirectory(t *testing.T) { runDir, err := NewRunDirectory("", RetentionAuto) if err != nil { t.Fatalf("NewRunDirectory: %v", err) } t.Cleanup(func() { _ = os.RemoveAll(runDir.Path()) _ = os.Remove(defaultWorkDir) }) if filepath.Dir(runDir.Path()) != defaultWorkDir { t.Fatalf("expected default work directory %q, got %q", defaultWorkDir, filepath.Dir(runDir.Path())) } } func TestWriteJSONArtifactWritesIndentedNewlineTerminatedJSON(t *testing.T) { runDir := newTestRunDirectory(t) if err := runDir.WriteJSONArtifact("artifact.json", map[string]any{"value": "ok"}); err != nil { t.Fatalf("WriteJSONArtifact: %v", err) } data := readArtifact(t, runDir, "artifact.json") if !strings.HasSuffix(string(data), "\n") { t.Fatalf("expected trailing newline, got %q", data) } if !strings.Contains(string(data), "\n \"value\": \"ok\"\n") { t.Fatalf("expected indented JSON, got %s", data) } } func TestWriteInvocationMetadataFillsMissingRunIDAndStartTime(t *testing.T) { runDir := newTestRunDirectory(t) if err := runDir.WriteInvocationMetadata(InvocationMetadata{Operation: "validate"}); err != nil { t.Fatalf("WriteInvocationMetadata: %v", err) } var got InvocationMetadata if err := json.Unmarshal(readArtifact(t, runDir, ArtifactInvocationMetadata), &got); err != nil { t.Fatalf("unmarshal invocation metadata: %v", err) } if got.RunID != runDir.RunID() { t.Fatalf("unexpected run ID: got %q want %q", got.RunID, runDir.RunID()) } if got.StartedAt.IsZero() { t.Fatalf("expected started_at to be filled") } if got.Operation != "validate" { t.Fatalf("unexpected operation: %q", got.Operation) } } func TestWriteInvocationMetadataPreservesProvidedRunIDAndStartTime(t *testing.T) { runDir := newTestRunDirectory(t) startedAt := time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC) if err := runDir.WriteInvocationMetadata(InvocationMetadata{ Operation: "validate", RunID: "provided", StartedAt: startedAt, }); err != nil { t.Fatalf("WriteInvocationMetadata: %v", err) } var got InvocationMetadata if err := json.Unmarshal(readArtifact(t, runDir, ArtifactInvocationMetadata), &got); err != nil { t.Fatalf("unmarshal invocation metadata: %v", err) } if got.RunID != "provided" { t.Fatalf("unexpected run ID: %q", got.RunID) } if !got.StartedAt.Equal(startedAt) { t.Fatalf("unexpected started_at: %s", got.StartedAt) } } func TestWriteTypedArtifacts(t *testing.T) { runDir := newTestRunDirectory(t) if err := runDir.WriteRedactedEffectiveConfig(map[string]any{"redacted": true}); err != nil { t.Fatalf("WriteRedactedEffectiveConfig: %v", err) } if err := runDir.WriteResolvedPipeline(map[string]any{"pipeline": "test"}); err != nil { t.Fatalf("WriteResolvedPipeline: %v", err) } if err := runDir.WriteSourceDocument(map[string]any{"source_id": "source-1"}); err != nil { t.Fatalf("WriteSourceDocument: %v", err) } if err := runDir.WriteRunManifest(artifacts.RunManifest{RunID: "run-1"}); err != nil { t.Fatalf("WriteRunManifest: %v", err) } if err := runDir.WriteRunReport(map[string]any{"ok": true}); err != nil { t.Fatalf("WriteRunReport: %v", err) } if err := runDir.WriteWarnings([]contracts.Warning{{ReasonCode: "test", Message: "warning"}}); err != nil { t.Fatalf("WriteWarnings: %v", err) } for _, name := range []string{ ArtifactEffectiveConfig, ArtifactResolvedPipeline, ArtifactSourceDocument, ArtifactRunManifest, ArtifactRunReport, ArtifactWarnings, } { if _, err := os.Stat(filepath.Join(runDir.Path(), name)); err != nil { t.Fatalf("expected artifact %q: %v", name, err) } } } func TestWriteErrorLogWritesPlainTextWithTrailingNewline(t *testing.T) { runDir := newTestRunDirectory(t) if err := runDir.WriteErrorLog("something failed"); err != nil { t.Fatalf("WriteErrorLog: %v", err) } if got := string(readArtifact(t, runDir, ArtifactErrorLog)); got != "something failed\n" { t.Fatalf("unexpected error log: %q", got) } } func TestArtifactPathRejectsUnsafeNames(t *testing.T) { runDir := newTestRunDirectory(t) tests := []string{ "", " ", "/absolute.json", "nested/artifact.json", `nested\artifact.json`, "../escape.json", } for _, name := range tests { t.Run(name, func(t *testing.T) { if err := runDir.WriteJSONArtifact(name, map[string]any{}); err == nil { t.Fatalf("expected unsafe artifact name %q to be rejected", name) } }) } } func TestShouldRetainRunDirectoryDecisions(t *testing.T) { tests := []struct { name string input RetentionDecisionInput want bool }{ {name: "failed auto retained", input: RetentionDecisionInput{RetentionMode: RetentionAuto, RunSucceeded: false}, want: true}, {name: "failed always retained", input: RetentionDecisionInput{RetentionMode: RetentionAlways, RunSucceeded: false}, want: true}, {name: "failed never retained", input: RetentionDecisionInput{RetentionMode: RetentionNever, RunSucceeded: false}, want: true}, {name: "successful always retained", input: RetentionDecisionInput{RetentionMode: RetentionAlways, RunSucceeded: true}, want: true}, {name: "successful never removed", input: RetentionDecisionInput{RetentionMode: RetentionNever, RunSucceeded: true}, want: false}, {name: "successful auto without warnings removed", input: RetentionDecisionInput{RetentionMode: RetentionAuto, RunSucceeded: true}, want: false}, {name: "successful auto with warnings retained", input: RetentionDecisionInput{RetentionMode: RetentionAuto, RunSucceeded: true, HasWarnings: true}, want: true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if got := ShouldRetainRunDirectory(tc.input); got != tc.want { t.Fatalf("ShouldRetainRunDirectory() = %v, want %v", got, tc.want) } }) } } func TestApplyRetentionRemovesOnlyRunDirectory(t *testing.T) { workDir := t.TempDir() runDir, err := NewRunDirectory(workDir, RetentionNever) if err != nil { t.Fatalf("NewRunDirectory: %v", err) } siblingPath := filepath.Join(workDir, "sibling") if err := os.WriteFile(siblingPath, []byte("keep"), 0o644); err != nil { t.Fatalf("write sibling: %v", err) } if err := runDir.ApplyRetention(RetentionDecisionInput{RunSucceeded: true}); err != nil { t.Fatalf("ApplyRetention: %v", err) } if _, err := os.Stat(runDir.Path()); !os.IsNotExist(err) { t.Fatalf("expected run directory removed, stat err=%v", err) } if _, err := os.Stat(workDir); err != nil { t.Fatalf("expected work directory retained: %v", err) } if _, err := os.Stat(siblingPath); err != nil { t.Fatalf("expected sibling retained: %v", err) } } func TestApplyRetentionKeepsRetainedRunDirectory(t *testing.T) { runDir := newTestRunDirectory(t) if err := runDir.ApplyRetention(RetentionDecisionInput{RunSucceeded: true, HasWarnings: true}); err != nil { t.Fatalf("ApplyRetention: %v", err) } if _, err := os.Stat(runDir.Path()); err != nil { t.Fatalf("expected run directory retained: %v", err) } } func newTestRunDirectory(t *testing.T) *RunDirectory { t.Helper() runDir, err := NewRunDirectory(t.TempDir(), RetentionAuto) if err != nil { t.Fatalf("NewRunDirectory: %v", err) } return runDir } func readArtifact(t *testing.T, runDir *RunDirectory, name string) []byte { t.Helper() data, err := os.ReadFile(filepath.Join(runDir.Path(), name)) if err != nil { t.Fatalf("read artifact %q: %v", name, err) } return data }