686 lines
19 KiB
Go
686 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/audita/internal/cli"
|
|
)
|
|
|
|
func TestHelperProcess(t *testing.T) {
|
|
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
|
return
|
|
}
|
|
|
|
sep := -1
|
|
for i, arg := range os.Args {
|
|
if arg == "--" {
|
|
sep = i
|
|
break
|
|
}
|
|
}
|
|
if sep == -1 {
|
|
os.Exit(2)
|
|
}
|
|
|
|
cli.ConfigureSubprocessTestHooksFromEnv()
|
|
code := cli.Run(os.Args[sep+1:], os.Stdout, os.Stderr)
|
|
os.Exit(code)
|
|
}
|
|
|
|
func TestProcessHelpSubprocess(t *testing.T) {
|
|
result := runCLISubprocess(t, "process", "--help")
|
|
if result.exitCode != 0 {
|
|
t.Fatalf("expected exit 0, got %d stderr=%q", result.exitCode, result.stderr)
|
|
}
|
|
if !strings.Contains(result.stdout, "Usage:") || !strings.Contains(result.stdout, "--glossary") {
|
|
t.Fatalf("unexpected help stdout: %q", result.stdout)
|
|
}
|
|
if result.stderr != "" {
|
|
t.Fatalf("expected empty stderr, got %q", result.stderr)
|
|
}
|
|
}
|
|
|
|
func TestProcessSuccessWithOutputSubprocess(t *testing.T) {
|
|
outputPath := filepath.Join(t.TempDir(), "corrected.json")
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--output",
|
|
outputPath,
|
|
)
|
|
|
|
if result.exitCode != 0 {
|
|
t.Fatalf("expected exit 0, got %d stderr=%q", result.exitCode, result.stderr)
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout when --output is set, got %q", result.stdout)
|
|
}
|
|
if result.stderr != "" {
|
|
t.Fatalf("expected empty stderr on success, got %q", result.stderr)
|
|
}
|
|
|
|
inputBytes := readFile(t, fixturePath("tiny_transcript.json"))
|
|
outputBytes := readFile(t, outputPath)
|
|
assertJSONSemanticallyEqual(t, inputBytes, outputBytes)
|
|
}
|
|
|
|
func TestProcessSuccessWithoutOutputSubprocess(t *testing.T) {
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
)
|
|
|
|
if result.exitCode != 0 {
|
|
t.Fatalf("expected exit 0, got %d stderr=%q", result.exitCode, result.stderr)
|
|
}
|
|
if result.stderr != "" {
|
|
t.Fatalf("expected empty stderr on success, got %q", result.stderr)
|
|
}
|
|
|
|
inputBytes := readFile(t, fixturePath("tiny_transcript.json"))
|
|
assertJSONSemanticallyEqual(t, inputBytes, []byte(result.stdout))
|
|
}
|
|
|
|
func TestProcessFailureMissingTranscriptSubprocess(t *testing.T) {
|
|
result := runCLISubprocess(t, "process", "--glossary", fixturePath("tiny_glossary.yaml"))
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "expected exactly 1 transcript JSON path argument") {
|
|
t.Fatalf("expected actionable missing transcript error, got %q", result.stderr)
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureMalformedJSONSubprocess(t *testing.T) {
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
fixturePath("malformed_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "is not valid JSON") {
|
|
t.Fatalf("expected malformed JSON error, got %q", result.stderr)
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureMissingTranscriptFileSubprocess(t *testing.T) {
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
filepath.Join(t.TempDir(), "missing-transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "transcript_read") {
|
|
t.Fatalf("expected transcript_read failure, got %q", result.stderr)
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureMissingGlossaryFileSubprocess(t *testing.T) {
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
filepath.Join(t.TempDir(), "missing-glossary.yaml"),
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "glossary_read") {
|
|
t.Fatalf("expected glossary_read failure, got %q", result.stderr)
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureTranscriptSchemaSubprocess(t *testing.T) {
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
schemaFixturePath("transcript_empty_speaker.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "transcript_schema") {
|
|
t.Fatalf("expected transcript_schema failure, got %q", result.stderr)
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureMalformedGlossaryYAMLSubprocess(t *testing.T) {
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
schemaFixturePath("glossary_malformed.yaml"),
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "glossary_schema") {
|
|
t.Fatalf("expected glossary_schema failure, got %q", result.stderr)
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureUnreadableTranscriptSubprocess(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("portable unreadable-file permissions are not reliable on windows")
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
transcriptPath := filepath.Join(dir, "transcript.json")
|
|
if err := os.WriteFile(transcriptPath, []byte(`[]`), 0o000); err != nil {
|
|
t.Fatalf("write unreadable transcript: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chmod(transcriptPath, 0o644) })
|
|
if _, err := os.ReadFile(transcriptPath); err == nil {
|
|
t.Skip("unable to make transcript unreadable on this platform/user")
|
|
}
|
|
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "transcript_read") {
|
|
t.Fatalf("expected transcript_read failure, got %q", result.stderr)
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureUnwritableOutputSubprocess(t *testing.T) {
|
|
outputDir := t.TempDir()
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--output",
|
|
outputDir,
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "failed to write output file") {
|
|
t.Fatalf("expected write failure message, got %q", result.stderr)
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureUnwritableReportJSONSubprocess(t *testing.T) {
|
|
reportDir := t.TempDir()
|
|
outputPath := filepath.Join(t.TempDir(), "out.json")
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--output",
|
|
outputPath,
|
|
"--report-json",
|
|
reportDir,
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "failed to write report JSON file") {
|
|
t.Fatalf("expected report write failure message, got %q", result.stderr)
|
|
}
|
|
}
|
|
|
|
func TestProcessSuccessReportJSONSubprocess(t *testing.T) {
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--report-json",
|
|
reportPath,
|
|
)
|
|
if result.exitCode != 0 {
|
|
t.Fatalf("expected exit 0, got %d stderr=%q", result.exitCode, result.stderr)
|
|
}
|
|
if result.stderr != "" {
|
|
t.Fatalf("expected empty stderr on success, got %q", result.stderr)
|
|
}
|
|
if !json.Valid([]byte(result.stdout)) {
|
|
t.Fatalf("expected transcript JSON only on stdout, got %q", result.stdout)
|
|
}
|
|
report := readFile(t, reportPath)
|
|
if !json.Valid(report) {
|
|
t.Fatalf("expected valid report JSON, got %q", string(report))
|
|
}
|
|
// Ensure report JSON is not printed to stdout.
|
|
if strings.Contains(result.stdout, `"phase16-default-pipeline-integration"`) {
|
|
t.Fatalf("report JSON leaked to stdout: %q", result.stdout)
|
|
}
|
|
}
|
|
|
|
func TestProcessSuccessLargeTranscriptSubprocess(t *testing.T) {
|
|
transcriptPath := writeLargeTranscriptFixture(t, 320)
|
|
result := runCLISubprocess(
|
|
t,
|
|
"process",
|
|
transcriptPath,
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
)
|
|
if result.exitCode != 0 {
|
|
t.Fatalf("expected exit 0, got %d stderr=%q", result.exitCode, result.stderr)
|
|
}
|
|
if result.stderr != "" {
|
|
t.Fatalf("expected empty stderr on success, got %q", result.stderr)
|
|
}
|
|
if !json.Valid([]byte(result.stdout)) {
|
|
t.Fatalf("expected valid transcript JSON on stdout")
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureMalformedStructuredLLMResponseViaSubprocessHook(t *testing.T) {
|
|
workDir := t.TempDir()
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
result := runCLISubprocessWithEnv(t,
|
|
map[string]string{"AUDITA_SUBPROCESS_TEST_LLM_MODE": "malformed_structured"},
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--modules",
|
|
"grammar",
|
|
"--report-json",
|
|
reportPath,
|
|
"--work-dir",
|
|
workDir,
|
|
"--work-dir-retention",
|
|
"always",
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "runner_execution") {
|
|
t.Fatalf("expected runner_execution failure, got %q", result.stderr)
|
|
}
|
|
if !strings.Contains(result.stderr, "diagnostics:") {
|
|
t.Fatalf("expected diagnostics path in stderr, got %q", result.stderr)
|
|
}
|
|
report := readFile(t, reportPath)
|
|
if !json.Valid(report) {
|
|
t.Fatalf("expected valid failure report JSON")
|
|
}
|
|
runDir := onlyRunDir(t, workDir)
|
|
if _, err := os.Stat(filepath.Join(runDir, "error.log")); err != nil {
|
|
t.Fatalf("expected error.log, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureBackendLLMViaSubprocessHook(t *testing.T) {
|
|
workDir := t.TempDir()
|
|
result := runCLISubprocessWithEnv(t,
|
|
map[string]string{"AUDITA_SUBPROCESS_TEST_LLM_MODE": "backend_error"},
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--modules",
|
|
"grammar",
|
|
"--work-dir",
|
|
workDir,
|
|
"--work-dir-retention",
|
|
"always",
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "synthetic backend failure") {
|
|
t.Fatalf("expected backend failure details, got %q", result.stderr)
|
|
}
|
|
if !strings.Contains(result.stderr, "diagnostics:") {
|
|
t.Fatalf("expected diagnostics path in stderr, got %q", result.stderr)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(onlyRunDir(t, workDir), "error.log")); err != nil {
|
|
t.Fatalf("expected error.log in retained failed run: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessFailureMidPipelinePreservesPartialReportsSubprocess(t *testing.T) {
|
|
workDir := t.TempDir()
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
result := runCLISubprocessWithEnv(t,
|
|
map[string]string{"AUDITA_SUBPROCESS_TEST_LLM_MODE": "mid_pipeline_fail"},
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--modules",
|
|
"glossary,homophones,glossary,spoken_word,grammar",
|
|
"--report-json",
|
|
reportPath,
|
|
"--work-dir",
|
|
workDir,
|
|
"--work-dir-retention",
|
|
"always",
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
reportRaw := readFile(t, reportPath)
|
|
var report struct {
|
|
Status string `json:"status"`
|
|
ErrorPhase string `json:"error_phase"`
|
|
ModuleResults []struct {
|
|
ModuleInstance string `json:"module_instance"`
|
|
Status string `json:"status"`
|
|
} `json:"module_results"`
|
|
}
|
|
if err := json.Unmarshal(reportRaw, &report); err != nil {
|
|
t.Fatalf("unmarshal report: %v", err)
|
|
}
|
|
if report.Status != "failed" || report.ErrorPhase != "runner_execution" {
|
|
t.Fatalf("expected failed runner_execution report, got %+v", report)
|
|
}
|
|
if len(report.ModuleResults) == 0 {
|
|
t.Fatalf("expected partial module results in failure report")
|
|
}
|
|
}
|
|
|
|
func TestProcessCancellationViaSubprocessTimeoutHook(t *testing.T) {
|
|
workDir := t.TempDir()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
result := runCLISubprocessContext(t, ctx,
|
|
map[string]string{
|
|
"AUDITA_SUBPROCESS_TEST_LLM_MODE": "block_until_cancel",
|
|
"AUDITA_SUBPROCESS_TEST_RUN_TIMEOUT_MS": "120",
|
|
},
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--modules",
|
|
"grammar",
|
|
"--work-dir",
|
|
workDir,
|
|
"--work-dir-retention",
|
|
"always",
|
|
)
|
|
if result.exitCode == 0 {
|
|
t.Fatalf("expected nonzero exit code")
|
|
}
|
|
if result.stdout != "" {
|
|
t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
|
|
}
|
|
if !strings.Contains(result.stderr, "context deadline exceeded") {
|
|
t.Fatalf("expected context deadline error, got %q", result.stderr)
|
|
}
|
|
runDir := onlyRunDir(t, workDir)
|
|
if _, err := os.Stat(filepath.Join(runDir, "error.log")); err != nil {
|
|
t.Fatalf("expected error.log for canceled run: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(runDir, "report.json")); err != nil {
|
|
t.Fatalf("expected report.json for canceled run: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessSubprocessNoSecretLeakInOutputsAndDiagnostics(t *testing.T) {
|
|
secret := "phase18-subprocess-secret"
|
|
workDir := t.TempDir()
|
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
|
outputPath := filepath.Join(t.TempDir(), "out.json")
|
|
result := runCLISubprocessWithEnv(t,
|
|
map[string]string{
|
|
"AUDITA_LLM_API_KEY": secret,
|
|
"AUDITA_VALIDATION_LLM_API_KEY": secret,
|
|
},
|
|
"process",
|
|
fixturePath("tiny_transcript.json"),
|
|
"--glossary",
|
|
fixturePath("tiny_glossary.yaml"),
|
|
"--output",
|
|
outputPath,
|
|
"--report-json",
|
|
reportPath,
|
|
"--work-dir",
|
|
workDir,
|
|
"--work-dir-retention",
|
|
"always",
|
|
)
|
|
if result.exitCode != 0 {
|
|
t.Fatalf("expected success, got %d stderr=%q", result.exitCode, result.stderr)
|
|
}
|
|
if strings.Contains(result.stdout, secret) || strings.Contains(result.stderr, secret) {
|
|
t.Fatalf("secret leaked in subprocess stdio")
|
|
}
|
|
assertNoSecretInFile(t, reportPath, secret)
|
|
assertNoSecretInTree(t, onlyRunDir(t, workDir), secret)
|
|
}
|
|
|
|
type subprocessResult struct {
|
|
stdout string
|
|
stderr string
|
|
exitCode int
|
|
}
|
|
|
|
func runCLISubprocess(t *testing.T, args ...string) subprocessResult {
|
|
t.Helper()
|
|
return runCLISubprocessWithEnv(t, nil, args...)
|
|
}
|
|
|
|
func runCLISubprocessWithEnv(t *testing.T, extraEnv map[string]string, args ...string) subprocessResult {
|
|
t.Helper()
|
|
return runCLISubprocessContext(t, context.Background(), extraEnv, args...)
|
|
}
|
|
|
|
func runCLISubprocessContext(t *testing.T, ctx context.Context, extraEnv map[string]string, args ...string) subprocessResult {
|
|
t.Helper()
|
|
cmdArgs := append([]string{"-test.run=TestHelperProcess", "--"}, args...)
|
|
cmd := exec.CommandContext(ctx, os.Args[0], cmdArgs...)
|
|
env := append(filterAuditaEnv(os.Environ()), "GO_WANT_HELPER_PROCESS=1")
|
|
for k, v := range extraEnv {
|
|
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
cmd.Env = env
|
|
var stdoutBuf bytes.Buffer
|
|
var stderrBuf bytes.Buffer
|
|
cmd.Stdout = &stdoutBuf
|
|
cmd.Stderr = &stderrBuf
|
|
|
|
err := cmd.Run()
|
|
result := subprocessResult{
|
|
stdout: stdoutBuf.String(),
|
|
stderr: stderrBuf.String(),
|
|
}
|
|
if err == nil {
|
|
return result
|
|
}
|
|
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
result.exitCode = exitErr.ExitCode()
|
|
return result
|
|
}
|
|
|
|
t.Fatalf("subprocess execution failed: %v", err)
|
|
return subprocessResult{}
|
|
}
|
|
|
|
func filterAuditaEnv(env []string) []string {
|
|
filtered := make([]string, 0, len(env))
|
|
for _, entry := range env {
|
|
key := entry
|
|
if idx := strings.IndexByte(entry, '='); idx >= 0 {
|
|
key = entry[:idx]
|
|
}
|
|
if strings.HasPrefix(key, "AUDITA_") || key == "OPENROUTER_API_KEY" {
|
|
continue
|
|
}
|
|
filtered = append(filtered, entry)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func fixturePath(name string) string {
|
|
return filepath.Join("..", "..", "internal", "cli", "testdata", name)
|
|
}
|
|
|
|
func schemaFixturePath(name string) string {
|
|
return filepath.Join("..", "..", "internal", "core", "schema", "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))
|
|
}
|
|
}
|
|
|
|
func writeLargeTranscriptFixture(t *testing.T, segments int) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "large-transcript.json")
|
|
rows := make([]string, 0, segments)
|
|
for i := 0; i < segments; i++ {
|
|
rows = append(rows, fmt.Sprintf(`{"id":%d,"speaker":"Speaker%d","start":%s,"end":%s,"text":"Segment %d has enough words to exercise stdout and pipe buffering safely."}`,
|
|
i+1,
|
|
(i%4)+1,
|
|
strconv.FormatFloat(float64(i)*1.1, 'f', 1, 64),
|
|
strconv.FormatFloat(float64(i)*1.1+1.0, 'f', 1, 64),
|
|
i+1,
|
|
))
|
|
}
|
|
payload := "[\n " + strings.Join(rows, ",\n ") + "\n]\n"
|
|
if err := os.WriteFile(path, []byte(payload), 0o644); err != nil {
|
|
t.Fatalf("write large transcript fixture: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func onlyRunDir(t *testing.T, workDir string) string {
|
|
t.Helper()
|
|
entries, err := os.ReadDir(workDir)
|
|
if err != nil {
|
|
t.Fatalf("failed to read work dir %q: %v", workDir, err)
|
|
}
|
|
dirs := make([]string, 0, len(entries))
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
dirs = append(dirs, filepath.Join(workDir, e.Name()))
|
|
}
|
|
}
|
|
if len(dirs) != 1 {
|
|
t.Fatalf("expected exactly one run dir in %q, found %d", workDir, len(dirs))
|
|
}
|
|
return dirs[0]
|
|
}
|
|
|
|
func assertNoSecretInFile(t *testing.T, path, secret string) {
|
|
t.Helper()
|
|
raw := string(readFile(t, path))
|
|
if strings.Contains(raw, secret) {
|
|
t.Fatalf("secret leaked in %s", path)
|
|
}
|
|
}
|
|
|
|
func assertNoSecretInTree(t *testing.T, root, secret string) {
|
|
t.Helper()
|
|
_ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil || d == nil || d.IsDir() {
|
|
return nil
|
|
}
|
|
raw, readErr := os.ReadFile(path)
|
|
if readErr == nil && strings.Contains(string(raw), secret) {
|
|
t.Fatalf("secret leaked in %s", path)
|
|
}
|
|
return nil
|
|
})
|
|
}
|