Add transcript passthrough processing
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
1
internal/cli/testdata/malformed_transcript.json
vendored
Normal file
1
internal/cli/testdata/malformed_transcript.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"segments":[{"id":1,"text":"oops"}
|
||||
3
internal/cli/testdata/tiny_glossary.yaml
vendored
Normal file
3
internal/cli/testdata/tiny_glossary.yaml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
terms:
|
||||
- source: audita
|
||||
target: Audita
|
||||
9
internal/cli/testdata/tiny_transcript.json
vendored
Normal file
9
internal/cli/testdata/tiny_transcript.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"speaker": "A",
|
||||
"start": 0.0,
|
||||
"end": 1.2,
|
||||
"text": "hello world"
|
||||
}
|
||||
]
|
||||
29
internal/core/io/files.go
Normal file
29
internal/core/io/files.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user