Add minimal process reports

This commit is contained in:
2026-05-10 23:30:46 +00:00
parent 2cf2d390da
commit 08b7531149
3 changed files with 231 additions and 0 deletions

View File

@@ -6,9 +6,11 @@ import (
"fmt"
"io"
"strings"
"time"
"gitea.maximumdirect.net/eric/audita/internal/core/config"
coreio "gitea.maximumdirect.net/eric/audita/internal/core/io"
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
)
type processInvocation struct {
@@ -66,6 +68,8 @@ func Run(args []string, stdout, stderr io.Writer) int {
}
func runProcess(args []string, stdout, stderr io.Writer) int {
startedAt := time.Now().UTC()
cfg, err := config.LoadFromEnv()
if err != nil {
fmt.Fprintf(stderr, "audita process: invalid environment configuration: %v\n", err)
@@ -182,13 +186,47 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
}
if err := processRunner(inv, stdout); err != nil {
completedAt := time.Now().UTC()
if strings.TrimSpace(inv.ReportJSONPath) != "" {
report := buildProcessReport("failed", inv, startedAt, completedAt, err.Error())
if reportErr := reporting.WriteProcessReport(inv.ReportJSONPath, report); reportErr != nil {
fmt.Fprintf(stderr, "audita process: %v\n", reportErr)
}
}
fmt.Fprintf(stderr, "audita process: %v\n", err)
return 1
}
if strings.TrimSpace(inv.ReportJSONPath) != "" {
completedAt := time.Now().UTC()
report := buildProcessReport("success", inv, startedAt, completedAt, "")
if err := reporting.WriteProcessReport(inv.ReportJSONPath, report); err != nil {
fmt.Fprintf(stderr, "audita process: %v\n", err)
return 1
}
}
return 0
}
func buildProcessReport(status string, inv processInvocation, startedAt, completedAt time.Time, errorMessage string) reporting.ProcessReport {
report := reporting.ProcessReport{
Phase: "phase1-minimal",
Status: status,
Operation: "process",
TranscriptPath: inv.TranscriptPath,
GlossaryPath: inv.GlossaryPath,
OutputPath: inv.OutputPath,
Modules: append([]string(nil), inv.Config.Modules...),
StartedAt: startedAt,
CompletedAt: &completedAt,
}
if errorMessage != "" {
report.ErrorMessage = errorMessage
}
return report
}
type processFlags struct {
glossaryPath *string
outputPath *string

View File

@@ -204,6 +204,143 @@ func TestRunProcessOutputToStdout(t *testing.T) {
assertJSONSemanticallyEqual(t, inputBytes, stdout.Bytes())
}
func TestRunProcessReportJSONSuccess(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
transcriptPath := fixturePath("tiny_transcript.json")
glossaryPath := fixturePath("tiny_glossary.yaml")
outputPath := filepath.Join(t.TempDir(), "corrected.json")
reportPath := filepath.Join(t.TempDir(), "report.json")
exitCode := Run([]string{
"process",
transcriptPath,
"--glossary",
glossaryPath,
"--output",
outputPath,
"--report-json",
reportPath,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
}
if stdout.Len() != 0 {
t.Fatalf("expected empty stdout when --output is used, got %q", stdout.String())
}
report := readProcessReport(t, reportPath)
if report.Status != "success" {
t.Fatalf("expected success report status, got %q", report.Status)
}
if report.Operation != "process" {
t.Fatalf("expected operation process, got %q", report.Operation)
}
if report.TranscriptPath != transcriptPath {
t.Fatalf("unexpected transcript path in report: %q", report.TranscriptPath)
}
if report.GlossaryPath != glossaryPath {
t.Fatalf("unexpected glossary path in report: %q", report.GlossaryPath)
}
if report.OutputPath != outputPath {
t.Fatalf("unexpected output path in report: %q", report.OutputPath)
}
if len(report.Modules) == 0 {
t.Fatalf("expected configured module sequence in report")
}
if report.StartedAt == "" || report.CompletedAt == "" {
t.Fatalf("expected started_at and completed_at timestamps in report")
}
if report.ErrorMessage != "" {
t.Fatalf("did not expect error message in success report, got %q", report.ErrorMessage)
}
}
func TestRunProcessReportJSONNoStdoutPollution(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
transcriptPath := fixturePath("tiny_transcript.json")
glossaryPath := fixturePath("tiny_glossary.yaml")
reportPath := filepath.Join(t.TempDir(), "report.json")
exitCode := Run([]string{
"process",
transcriptPath,
"--glossary",
glossaryPath,
"--report-json",
reportPath,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
}
assertJSONSemanticallyEqual(t, readFile(t, transcriptPath), stdout.Bytes())
reportBytes := readFile(t, reportPath)
if bytes.Contains(stdout.Bytes(), reportBytes) {
t.Fatalf("stdout was polluted with report JSON")
}
}
func TestRunProcessReportJSONRedactsSecrets(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
secretPrimary := "top-secret-primary-key"
secretValidation := "top-secret-validation-key"
t.Setenv("AUDITA_LLM_API_KEY", secretPrimary)
t.Setenv("AUDITA_VALIDATION_LLM_API_KEY", secretValidation)
reportPath := filepath.Join(t.TempDir(), "report.json")
exitCode := Run([]string{
"process",
fixturePath("tiny_transcript.json"),
"--glossary",
fixturePath("tiny_glossary.yaml"),
"--report-json",
reportPath,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
}
reportBytes := readFile(t, reportPath)
if strings.Contains(string(reportBytes), secretPrimary) || strings.Contains(string(reportBytes), secretValidation) {
t.Fatalf("report JSON leaked API key material: %q", string(reportBytes))
}
}
func TestRunProcessReportJSONBestEffortOnFailure(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
reportPath := filepath.Join(t.TempDir(), "report.json")
exitCode := Run([]string{
"process",
fixturePath("malformed_transcript.json"),
"--glossary",
fixturePath("tiny_glossary.yaml"),
"--report-json",
reportPath,
}, &stdout, &stderr)
if exitCode == 0 {
t.Fatalf("expected nonzero exit code for malformed transcript")
}
report := readProcessReport(t, reportPath)
if report.Status != "failed" {
t.Fatalf("expected failed report status, got %q", report.Status)
}
if report.ErrorMessage == "" {
t.Fatalf("expected error message in failed report")
}
if !strings.Contains(report.ErrorMessage, "not valid JSON") {
t.Fatalf("expected malformed JSON failure in report error, got %q", report.ErrorMessage)
}
}
func TestRunProcessCLIOverridesEnvironment(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
@@ -281,3 +418,25 @@ func assertJSONSemanticallyEqual(t *testing.T, expected []byte, actual []byte) {
t.Fatalf("JSON content mismatch: expected %q got %q", string(expected), string(actual))
}
}
type minimalProcessReport struct {
Status string `json:"status"`
Operation string `json:"operation"`
TranscriptPath string `json:"transcript_path"`
GlossaryPath string `json:"glossary_path"`
OutputPath string `json:"output_path"`
Modules []string `json:"modules"`
StartedAt string `json:"started_at"`
CompletedAt string `json:"completed_at"`
ErrorMessage string `json:"error_message"`
}
func readProcessReport(t *testing.T, path string) minimalProcessReport {
t.Helper()
raw := readFile(t, path)
var report minimalProcessReport
if err := json.Unmarshal(raw, &report); err != nil {
t.Fatalf("failed to unmarshal process report JSON: %v (raw: %q)", err, string(raw))
}
return report
}