From 2cf2d390dadb8c599a6f90c151bf2ce5d0f8e3de Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 10 May 2026 23:27:54 +0000 Subject: [PATCH] Add transcript passthrough processing --- internal/cli/run.go | 31 ++++- internal/cli/run_test.go | 128 +++++++++++++++--- .../cli/testdata/malformed_transcript.json | 1 + internal/cli/testdata/tiny_glossary.yaml | 3 + internal/cli/testdata/tiny_transcript.json | 9 ++ internal/core/io/files.go | 29 ++++ 6 files changed, 173 insertions(+), 28 deletions(-) create mode 100644 internal/cli/testdata/malformed_transcript.json create mode 100644 internal/cli/testdata/tiny_glossary.yaml create mode 100644 internal/cli/testdata/tiny_transcript.json create mode 100644 internal/core/io/files.go diff --git a/internal/cli/run.go b/internal/cli/run.go index 17d94b0..52dfe39 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -8,10 +8,9 @@ import ( "strings" "gitea.maximumdirect.net/eric/audita/internal/core/config" + coreio "gitea.maximumdirect.net/eric/audita/internal/core/io" ) -const processNotImplementedMessage = "process command is not implemented yet" - type processInvocation struct { TranscriptPath string GlossaryPath string @@ -20,9 +19,29 @@ type processInvocation struct { Config config.Config } -var processRunner = func(inv processInvocation) error { - _ = inv - return errors.New(processNotImplementedMessage) +var processRunner = func(inv processInvocation, stdout io.Writer) error { + transcriptBytes, err := coreio.ReadRequiredFile(inv.TranscriptPath, "transcript") + if err != nil { + return err + } + if err := coreio.ValidateWellFormedJSON(transcriptBytes); err != nil { + return err + } + if _, err := coreio.ReadRequiredFile(inv.GlossaryPath, "glossary"); err != nil { + return err + } + + if strings.TrimSpace(inv.OutputPath) != "" { + if err := coreio.WriteFile(inv.OutputPath, transcriptBytes); err != nil { + return err + } + return nil + } + + if _, err := stdout.Write(transcriptBytes); err != nil { + return fmt.Errorf("failed to write transcript to stdout: %w", err) + } + return nil } // Run executes the Audita CLI with the provided arguments and streams. @@ -162,7 +181,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int { Config: cfg, } - if err := processRunner(inv); err != nil { + if err := processRunner(inv, stdout); err != nil { fmt.Fprintf(stderr, "audita process: %v\n", err) return 1 } diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index d9a43ef..77c7689 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -2,7 +2,11 @@ package cli import ( "bytes" - "errors" + "encoding/json" + "io" + "os" + "path/filepath" + "reflect" "strings" "testing" ) @@ -96,7 +100,7 @@ func TestRunProcessMissingTranscriptPath(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer - exitCode := Run([]string{"process", "--glossary", "glossary.yaml"}, &stdout, &stderr) + exitCode := Run([]string{"process", "--glossary", fixturePath("tiny_glossary.yaml")}, &stdout, &stderr) if exitCode == 0 { t.Fatalf("expected nonzero exit code") } @@ -112,7 +116,7 @@ func TestRunProcessMissingGlossary(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer - exitCode := Run([]string{"process", "transcript.json"}, &stdout, &stderr) + exitCode := Run([]string{"process", fixturePath("tiny_transcript.json")}, &stdout, &stderr) if exitCode == 0 { t.Fatalf("expected nonzero exit code") } @@ -124,38 +128,82 @@ func TestRunProcessMissingGlossary(t *testing.T) { } } -func TestRunProcessInvalidCLIConfig(t *testing.T) { +func TestRunProcessUnreadableTranscript(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer - exitCode := Run([]string{"process", "transcript.json", "--glossary", "glossary.yaml", "--work-dir-retention", "invalid"}, &stdout, &stderr) - if exitCode != 2 { - t.Fatalf("expected exit code 2, got %d", exitCode) + missingTranscript := filepath.Join(t.TempDir(), "missing.json") + exitCode := Run([]string{"process", missingTranscript, "--glossary", fixturePath("tiny_glossary.yaml")}, &stdout, &stderr) + if exitCode == 0 { + t.Fatalf("expected nonzero exit code") } if stdout.Len() != 0 { t.Fatalf("expected empty stdout, got %q", stdout.String()) } - if !strings.Contains(stderr.String(), "invalid CLI configuration") { - t.Fatalf("expected invalid config error, got %q", stderr.String()) + if !strings.Contains(stderr.String(), "failed to read transcript file") { + t.Fatalf("expected unreadable transcript error, got %q", stderr.String()) } } -func TestRunProcessNotImplemented(t *testing.T) { +func TestRunProcessMalformedTranscriptJSON(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer - exitCode := Run([]string{"process", "transcript.json", "--glossary", "glossary.yaml"}, &stdout, &stderr) - if exitCode != 1 { - t.Fatalf("expected exit code 1 for not-implemented process, got %d", exitCode) + exitCode := Run([]string{"process", fixturePath("malformed_transcript.json"), "--glossary", fixturePath("tiny_glossary.yaml")}, &stdout, &stderr) + if exitCode == 0 { + t.Fatalf("expected nonzero exit code") } if stdout.Len() != 0 { t.Fatalf("expected empty stdout, got %q", stdout.String()) } - if !strings.Contains(stderr.String(), processNotImplementedMessage) { - t.Fatalf("expected not-implemented message in stderr, got %q", stderr.String()) + if !strings.Contains(stderr.String(), "transcript file is not valid JSON") { + t.Fatalf("expected malformed transcript error, got %q", stderr.String()) } } +func TestRunProcessOutputToFile(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") + + exitCode := Run([]string{"process", transcriptPath, "--glossary", glossaryPath, "--output", outputPath}, &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()) + } + if stderr.Len() != 0 { + t.Fatalf("expected empty stderr on success, got %q", stderr.String()) + } + + inputBytes := readFile(t, transcriptPath) + outputBytes := readFile(t, outputPath) + assertJSONSemanticallyEqual(t, inputBytes, outputBytes) +} + +func TestRunProcessOutputToStdout(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + transcriptPath := fixturePath("tiny_transcript.json") + glossaryPath := fixturePath("tiny_glossary.yaml") + + exitCode := Run([]string{"process", transcriptPath, "--glossary", glossaryPath}, &stdout, &stderr) + if exitCode != 0 { + t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String()) + } + if stderr.Len() != 0 { + t.Fatalf("expected empty stderr on success, got %q", stderr.String()) + } + + inputBytes := readFile(t, transcriptPath) + assertJSONSemanticallyEqual(t, inputBytes, stdout.Bytes()) +} + func TestRunProcessCLIOverridesEnvironment(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer @@ -163,11 +211,15 @@ func TestRunProcessCLIOverridesEnvironment(t *testing.T) { t.Setenv("AUDITA_MODEL", "env-model") t.Setenv("AUDITA_VALIDATION_LLM_CONCURRENCY", "2") + transcriptPath := fixturePath("tiny_transcript.json") + glossaryPath := fixturePath("tiny_glossary.yaml") + var captured processInvocation originalRunner := processRunner - processRunner = func(inv processInvocation) error { + processRunner = func(inv processInvocation, output io.Writer) error { + _ = output captured = inv - return errors.New(processNotImplementedMessage) + return nil } t.Cleanup(func() { processRunner = originalRunner @@ -175,17 +227,17 @@ func TestRunProcessCLIOverridesEnvironment(t *testing.T) { exitCode := Run([]string{ "process", - "transcript.json", + transcriptPath, "--glossary", - "glossary.yaml", + glossaryPath, "--model", "cli-model", "--validation-llm-concurrency", "5", }, &stdout, &stderr) - if exitCode != 1 { - t.Fatalf("expected exit code 1 for not implemented, got %d", exitCode) + if exitCode != 0 { + t.Fatalf("expected exit code 0, got %d", exitCode) } if captured.Config.PrimaryLLM.Model != "cli-model" { t.Fatalf("expected CLI model override, got %q", captured.Config.PrimaryLLM.Model) @@ -193,7 +245,39 @@ func TestRunProcessCLIOverridesEnvironment(t *testing.T) { if captured.Config.ValidationLLM.Concurrency == nil || *captured.Config.ValidationLLM.Concurrency != 5 { t.Fatalf("expected CLI validation concurrency override, got %#v", captured.Config.ValidationLLM.Concurrency) } - if captured.GlossaryPath != "glossary.yaml" { + if captured.GlossaryPath != glossaryPath { t.Fatalf("unexpected glossary path: %q", captured.GlossaryPath) } } + +func fixturePath(name string) string { + return filepath.Join("testdata", name) +} + +func readFile(t *testing.T, path string) []byte { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read file %q: %v", path, err) + } + return data +} + +func assertJSONSemanticallyEqual(t *testing.T, expected []byte, actual []byte) { + t.Helper() + if !json.Valid(actual) { + t.Fatalf("actual output is not valid JSON: %q", string(actual)) + } + + var expectedValue any + var actualValue any + if err := json.Unmarshal(expected, &expectedValue); err != nil { + t.Fatalf("failed to unmarshal expected JSON: %v", err) + } + if err := json.Unmarshal(actual, &actualValue); err != nil { + t.Fatalf("failed to unmarshal actual JSON: %v", err) + } + if !reflect.DeepEqual(expectedValue, actualValue) { + t.Fatalf("JSON content mismatch: expected %q got %q", string(expected), string(actual)) + } +} diff --git a/internal/cli/testdata/malformed_transcript.json b/internal/cli/testdata/malformed_transcript.json new file mode 100644 index 0000000..7ca0772 --- /dev/null +++ b/internal/cli/testdata/malformed_transcript.json @@ -0,0 +1 @@ +{"segments":[{"id":1,"text":"oops"} diff --git a/internal/cli/testdata/tiny_glossary.yaml b/internal/cli/testdata/tiny_glossary.yaml new file mode 100644 index 0000000..77e6563 --- /dev/null +++ b/internal/cli/testdata/tiny_glossary.yaml @@ -0,0 +1,3 @@ +terms: + - source: audita + target: Audita diff --git a/internal/cli/testdata/tiny_transcript.json b/internal/cli/testdata/tiny_transcript.json new file mode 100644 index 0000000..6ea9bea --- /dev/null +++ b/internal/cli/testdata/tiny_transcript.json @@ -0,0 +1,9 @@ +[ + { + "id": 1, + "speaker": "A", + "start": 0.0, + "end": 1.2, + "text": "hello world" + } +] diff --git a/internal/core/io/files.go b/internal/core/io/files.go new file mode 100644 index 0000000..efc4c4c --- /dev/null +++ b/internal/core/io/files.go @@ -0,0 +1,29 @@ +package io + +import ( + "encoding/json" + "fmt" + "os" +) + +func ReadRequiredFile(path string, label string) ([]byte, error) { + contents, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read %s file %q: %w", label, path, err) + } + return contents, nil +} + +func ValidateWellFormedJSON(raw []byte) error { + if !json.Valid(raw) { + return fmt.Errorf("transcript file is not valid JSON") + } + return nil +} + +func WriteFile(path string, contents []byte) error { + if err := os.WriteFile(path, contents, 0o644); err != nil { + return fmt.Errorf("failed to write output file %q: %w", path, err) + } + return nil +}