package diagnostics import ( "encoding/json" "fmt" "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 TestNewRunDirectoryRetriesOnRunIDCollision(t *testing.T) { workDir := t.TempDir() first := time.Unix(0, 100).UTC() second := first.Add(time.Nanosecond) if err := os.Mkdir(filepath.Join(workDir, fmt.Sprintf("run-%d", first.UnixNano())), 0o755); err != nil { t.Fatalf("create existing run directory: %v", err) } restoreUTCNow := replaceUTCNow(func() func() time.Time { calls := 0 return func() time.Time { calls++ if calls == 1 { return first } return second } }()) t.Cleanup(restoreUTCNow) runDir, err := NewRunDirectory(workDir, RetentionAuto) if err != nil { t.Fatalf("NewRunDirectory: %v", err) } wantRunID := fmt.Sprintf("run-%d", second.UnixNano()) if runDir.RunID() != wantRunID { t.Fatalf("RunID = %q, want %q", runDir.RunID(), wantRunID) } if _, err := os.Stat(runDir.Path()); err != nil { t.Fatalf("stat run directory: %v", err) } } func TestNewRunDirectoryReturnsErrorAfterRunIDCollisionsExhausted(t *testing.T) { workDir := t.TempDir() collisionTime := time.Unix(0, 200).UTC() collisionPath := filepath.Join(workDir, fmt.Sprintf("run-%d", collisionTime.UnixNano())) if err := os.Mkdir(collisionPath, 0o755); err != nil { t.Fatalf("create existing run directory: %v", err) } restoreUTCNow := replaceUTCNow(func() time.Time { return collisionTime }) t.Cleanup(restoreUTCNow) _, err := NewRunDirectory(workDir, RetentionAuto) if err == nil || !strings.Contains(err.Error(), "exhausted unique run ID attempts") { t.Fatalf("expected exhausted collision error, got %v", err) } } 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 TestWriteJSONArtifactLeavesNoTemporaryFiles(t *testing.T) { runDir := newTestRunDirectory(t) if err := runDir.WriteJSONArtifact("artifact.json", map[string]any{"value": "ok"}); err != nil { t.Fatalf("WriteJSONArtifact: %v", err) } entries, err := os.ReadDir(runDir.Path()) if err != nil { t.Fatalf("read run directory: %v", err) } for _, entry := range entries { if strings.Contains(entry.Name(), ".tmp-") { t.Fatalf("temporary diagnostics file remains after success: %s", entry.Name()) } } } 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(fakeRedactedEffectiveConfig{payload: 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.WriteResolvedReferences([]artifacts.ReferenceProvenance{{LaneID: "events", SlotName: "roster"}}); err != nil { t.Fatalf("WriteResolvedReferences: %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, ArtifactResolvedReferences, ArtifactSourceDocument, ArtifactRunManifest, ArtifactRunReport, ArtifactWarnings, } { if _, err := os.Stat(filepath.Join(runDir.Path(), name)); err != nil { t.Fatalf("expected artifact %q: %v", name, err) } } } func TestWriteRedactedEffectiveConfigWritesPayloadReturnedByProvider(t *testing.T) { runDir := newTestRunDirectory(t) if err := runDir.WriteRedactedEffectiveConfig(fakeRedactedEffectiveConfig{ payload: map[string]any{ "api_key": "[REDACTED]", "model": "test-model", }, }); err != nil { t.Fatalf("WriteRedactedEffectiveConfig: %v", err) } data := string(readArtifact(t, runDir, ArtifactEffectiveConfig)) if !strings.Contains(data, `"api_key": "[REDACTED]"`) || !strings.Contains(data, `"model": "test-model"`) { t.Fatalf("unexpected effective config artifact: %s", data) } } 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 replaceUTCNow(replacement func() time.Time) func() { original := utcNow utcNow = replacement return func() { utcNow = original } } 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 } type fakeRedactedEffectiveConfig struct { payload any } func (f fakeRedactedEffectiveConfig) RedactedDiagnosticsPayload() any { return f.payload }