371 lines
11 KiB
Go
371 lines
11 KiB
Go
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
|
|
}
|