From 79a8bcf37b0d908bf0f322371781aea8d1dbae68 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 8 May 2026 16:58:26 +0000 Subject: [PATCH] Add Seriatim trim adapter support --- internal/adapters/seriatim/fake.go | 97 ++++++++- internal/adapters/seriatim/fake_test.go | 48 +++++ internal/adapters/seriatim/runner.go | 30 ++- internal/adapters/seriatim/subprocess.go | 136 +++++++++++- internal/adapters/seriatim/subprocess_test.go | 197 +++++++++++++++++- 5 files changed, 495 insertions(+), 13 deletions(-) diff --git a/internal/adapters/seriatim/fake.go b/internal/adapters/seriatim/fake.go index 59ad493..4133985 100644 --- a/internal/adapters/seriatim/fake.go +++ b/internal/adapters/seriatim/fake.go @@ -29,11 +29,33 @@ func (n *NoopRunner) Run(ctx context.Context, req MergeRequest) (MergeResult, er }, nil } +// Trim returns the requested output path with placeholder metadata. +func (n *NoopRunner) Trim(ctx context.Context, req TrimRequest) (TrimResult, error) { + if err := ctx.Err(); err != nil { + return TrimResult{}, err + } + if err := materializeTrimPlaceholders(req); err != nil { + return TrimResult{}, err + } + return TrimResult{ + OutputTrimmedPath: req.OutputTrimmedPath, + StdoutLogPath: req.StdoutLogPath, + StderrLogPath: req.StderrLogPath, + GeneratedConfigPath: req.GeneratedConfigPath, + InvokedBinary: "noop", + KeepSelector: req.KeepSelector, + Metadata: map[string]any{"placeholder": true}, + }, nil +} + // FakeRunner captures merge requests and returns deterministic responses. type FakeRunner struct { - Requests []MergeRequest - Err error - Result MergeResult + Requests []MergeRequest + Err error + Result MergeResult + TrimRequests []TrimRequest + TrimErr error + TrimResult TrimResult } // Run records request and returns configured response. @@ -73,6 +95,43 @@ func (f *FakeRunner) Run(ctx context.Context, req MergeRequest) (MergeResult, er return res, nil } +// Trim records request and returns configured response. +func (f *FakeRunner) Trim(ctx context.Context, req TrimRequest) (TrimResult, error) { + if err := ctx.Err(); err != nil { + return TrimResult{}, err + } + f.TrimRequests = append(f.TrimRequests, req) + if f.TrimErr != nil { + return TrimResult{}, f.TrimErr + } + if err := materializeTrimPlaceholders(req); err != nil { + return TrimResult{}, err + } + res := f.TrimResult + if res.OutputTrimmedPath == "" { + res.OutputTrimmedPath = req.OutputTrimmedPath + } + if res.StdoutLogPath == "" { + res.StdoutLogPath = req.StdoutLogPath + } + if res.StderrLogPath == "" { + res.StderrLogPath = req.StderrLogPath + } + if res.GeneratedConfigPath == "" { + res.GeneratedConfigPath = req.GeneratedConfigPath + } + if res.InvokedBinary == "" { + res.InvokedBinary = "fake" + } + if res.KeepSelector == "" { + res.KeepSelector = req.KeepSelector + } + if res.Metadata == nil { + res.Metadata = map[string]any{"fake": true} + } + return res, nil +} + func materializePlaceholders(req MergeRequest) error { if req.OutputMergedTranscriptPath != "" { if err := subprocess.WriteFileAtomic(req.OutputMergedTranscriptPath, []byte(`{"schema":"seriatim.intermediate.v1","segments":[]}`), 0o644); err != nil { @@ -107,3 +166,35 @@ func materializePlaceholders(req MergeRequest) error { } return nil } + +func materializeTrimPlaceholders(req TrimRequest) error { + if req.OutputTrimmedPath != "" { + if err := subprocess.WriteFileAtomic(req.OutputTrimmedPath, []byte(`{"schema":"seriatim.intermediate.v1","segments":[]}`), 0o644); err != nil { + return fmt.Errorf("write trimmed transcript %q: %w", req.OutputTrimmedPath, err) + } + } + if req.GeneratedConfigPath != "" { + payload := map[string]any{ + "schema": "seriatim.generated.v1", + "placeholder": true, + "command": "trim", + "input_path": req.InputTranscriptPath, + "output_path": req.OutputTrimmedPath, + "keep_selector": req.KeepSelector, + } + if err := subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644); err != nil { + return fmt.Errorf("write generated config %q: %w", req.GeneratedConfigPath, err) + } + } + if req.StdoutLogPath != "" { + if err := subprocess.WriteFileAtomic(req.StdoutLogPath, []byte("seriatim noop/fake trim stdout placeholder\n"), 0o644); err != nil { + return fmt.Errorf("write stdout log %q: %w", req.StdoutLogPath, err) + } + } + if req.StderrLogPath != "" { + if err := subprocess.WriteFileAtomic(req.StderrLogPath, []byte("seriatim noop/fake trim stderr placeholder\n"), 0o644); err != nil { + return fmt.Errorf("write stderr log %q: %w", req.StderrLogPath, err) + } + } + return nil +} diff --git a/internal/adapters/seriatim/fake_test.go b/internal/adapters/seriatim/fake_test.go index 613b7d2..8a9c743 100644 --- a/internal/adapters/seriatim/fake_test.go +++ b/internal/adapters/seriatim/fake_test.go @@ -51,3 +51,51 @@ func TestFakeRunnerError(t *testing.T) { t.Fatal("expected error, got nil") } } + +func TestFakeRunnerTrimCapturesRequestAndReturnsPath(t *testing.T) { + fake := &FakeRunner{} + dir := t.TempDir() + req := TrimRequest{ + GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.trim.yml"), + InputTranscriptPath: filepath.Join(dir, "transcripts", "processed.json"), + OutputTrimmedPath: filepath.Join(dir, "transcripts", "trimmed.json"), + KeepSelector: "1-10", + StdoutLogPath: filepath.Join(dir, "logs", "seriatim.trim.stdout.log"), + StderrLogPath: filepath.Join(dir, "logs", "seriatim.trim.stderr.log"), + } + + res, err := fake.Trim(context.Background(), req) + if err != nil { + t.Fatalf("Trim() error = %v", err) + } + if len(fake.TrimRequests) != 1 || fake.TrimRequests[0].GeneratedConfigPath == "" { + t.Fatalf("trim requests = %#v, want captured request", fake.TrimRequests) + } + if res.OutputTrimmedPath != req.OutputTrimmedPath { + t.Fatalf("trimmed path = %q, want %q", res.OutputTrimmedPath, req.OutputTrimmedPath) + } + if res.KeepSelector != req.KeepSelector { + t.Fatalf("keep selector = %q, want %q", res.KeepSelector, req.KeepSelector) + } + + cfgData, err := os.ReadFile(req.GeneratedConfigPath) + if err != nil { + t.Fatalf("read generated config: %v", err) + } + if !strings.Contains(string(cfgData), "command: trim") { + t.Fatalf("generated config = %q, want trim command marker", string(cfgData)) + } + for _, logPath := range []string{req.StdoutLogPath, req.StderrLogPath} { + if _, err := os.Stat(logPath); err != nil { + t.Fatalf("expected log file %q to exist: %v", logPath, err) + } + } +} + +func TestFakeRunnerTrimError(t *testing.T) { + fake := &FakeRunner{TrimErr: errors.New("boom")} + _, err := fake.Trim(context.Background(), TrimRequest{}) + if err == nil { + t.Fatal("expected error, got nil") + } +} diff --git a/internal/adapters/seriatim/runner.go b/internal/adapters/seriatim/runner.go index 92d5b14..17bd295 100644 --- a/internal/adapters/seriatim/runner.go +++ b/internal/adapters/seriatim/runner.go @@ -1,4 +1,4 @@ -// Package seriatim declares the adapter contract for transcript merge execution. +// Package seriatim declares the adapter contract for transcript merge/trim execution. package seriatim import ( @@ -8,9 +8,10 @@ import ( // TODO: implement a real Seriatim subprocess adapter. -// Runner is the adapter boundary for seriatim merge invocations. +// Runner is the adapter boundary for seriatim merge/trim invocations. type Runner interface { Run(ctx context.Context, req MergeRequest) (MergeResult, error) + Trim(ctx context.Context, req TrimRequest) (TrimResult, error) } // MergeRequest describes a seriatim merge invocation. @@ -38,3 +39,28 @@ type MergeResult struct { OutputSchema string Metadata map[string]any } + +// TrimRequest describes a seriatim trim invocation. +type TrimRequest struct { + Binary string + InputTranscriptPath string + OutputTrimmedPath string + KeepSelector string + StdoutLogPath string + StderrLogPath string + GeneratedConfigPath string + Timeout time.Duration +} + +// TrimResult describes a trim output. +type TrimResult struct { + OutputTrimmedPath string + StdoutLogPath string + StderrLogPath string + GeneratedConfigPath string + ExitCode int + Duration time.Duration + InvokedBinary string + KeepSelector string + Metadata map[string]any +} diff --git a/internal/adapters/seriatim/subprocess.go b/internal/adapters/seriatim/subprocess.go index 3b9f8b1..de13af2 100644 --- a/internal/adapters/seriatim/subprocess.go +++ b/internal/adapters/seriatim/subprocess.go @@ -111,11 +111,11 @@ func (r *SubprocessRunner) Run(ctx context.Context, req MergeRequest) (MergeResu return MergeResult{}, fmt.Errorf("seriatim report is enabled but report path is missing") } - args := r.buildArgs(req) + args := r.buildMergeArgs(req) env := r.buildEnvOverrides() if req.GeneratedConfigPath != "" { - if err := r.writeInvocationConfig(req, args); err != nil { + if err := r.writeMergeInvocationConfig(req, args); err != nil { return MergeResult{}, fmt.Errorf("write seriatim invocation config %q: %w", req.GeneratedConfigPath, err) } } @@ -188,7 +188,90 @@ func (r *SubprocessRunner) Run(ctx context.Context, req MergeRequest) (MergeResu }, nil } -func (r *SubprocessRunner) buildArgs(req MergeRequest) []string { +// Trim executes Seriatim trim with deterministic flags and validates output artifacts. +func (r *SubprocessRunner) Trim(ctx context.Context, req TrimRequest) (TrimResult, error) { + if r == nil { + return TrimResult{}, fmt.Errorf("seriatim subprocess runner is nil") + } + if strings.TrimSpace(req.InputTranscriptPath) == "" { + return TrimResult{}, fmt.Errorf("seriatim trim input path is required") + } + if strings.TrimSpace(req.OutputTrimmedPath) == "" { + return TrimResult{}, fmt.Errorf("seriatim trim output path is required") + } + if strings.TrimSpace(req.KeepSelector) == "" { + return TrimResult{}, fmt.Errorf("seriatim trim keep selector is required") + } + + binary := r.binary + if strings.TrimSpace(req.Binary) != "" { + binary = strings.TrimSpace(req.Binary) + } + + timeout := r.timeout + if req.Timeout < 0 { + return TrimResult{}, fmt.Errorf("seriatim trim timeout must be >= 0") + } + if req.Timeout > 0 { + timeout = req.Timeout + } + + args := buildTrimArgs(req) + if req.GeneratedConfigPath != "" { + if err := writeTrimInvocationConfig(req, args, binary, timeout); err != nil { + return TrimResult{}, fmt.Errorf("write seriatim trim invocation config %q: %w", req.GeneratedConfigPath, err) + } + } + + runRes, err := subprocess.Run(ctx, subprocess.RunRequest{ + Executable: binary, + Args: args, + Timeout: timeout, + StdoutLogPath: req.StdoutLogPath, + StderrLogPath: req.StderrLogPath, + }) + if err != nil { + return TrimResult{ + OutputTrimmedPath: req.OutputTrimmedPath, + StdoutLogPath: req.StdoutLogPath, + StderrLogPath: req.StderrLogPath, + GeneratedConfigPath: req.GeneratedConfigPath, + ExitCode: runRes.ExitCode, + Duration: runRes.Duration, + InvokedBinary: binary, + KeepSelector: req.KeepSelector, + }, fmt.Errorf("run seriatim trim (binary=%q): %w", binary, err) + } + + if err := validateJSONFileWithSegments(req.OutputTrimmedPath); err != nil { + return TrimResult{ + OutputTrimmedPath: req.OutputTrimmedPath, + StdoutLogPath: req.StdoutLogPath, + StderrLogPath: req.StderrLogPath, + GeneratedConfigPath: req.GeneratedConfigPath, + ExitCode: runRes.ExitCode, + Duration: runRes.Duration, + InvokedBinary: binary, + KeepSelector: req.KeepSelector, + }, fmt.Errorf("validate seriatim trimmed output %q: %w", req.OutputTrimmedPath, err) + } + + return TrimResult{ + OutputTrimmedPath: req.OutputTrimmedPath, + StdoutLogPath: req.StdoutLogPath, + StderrLogPath: req.StderrLogPath, + GeneratedConfigPath: req.GeneratedConfigPath, + ExitCode: runRes.ExitCode, + Duration: runRes.Duration, + InvokedBinary: binary, + KeepSelector: req.KeepSelector, + Metadata: map[string]any{ + "adapter": "seriatim_subprocess", + }, + }, nil +} + +func (r *SubprocessRunner) buildMergeArgs(req MergeRequest) []string { args := []string{"merge"} for _, path := range req.InputTranscriptPaths { @@ -234,10 +317,11 @@ func (r *SubprocessRunner) buildEnvOverrides() map[string]string { return out } -func (r *SubprocessRunner) writeInvocationConfig(req MergeRequest, args []string) error { +func (r *SubprocessRunner) writeMergeInvocationConfig(req MergeRequest, args []string) error { payload := map[string]any{ "schema": "seriatim.generated.v1", "binary": r.binary, + "command": "merge", "args": args, "timeout": r.timeout.String(), "output_schema": r.outputSchema, @@ -261,6 +345,29 @@ func (r *SubprocessRunner) writeInvocationConfig(req MergeRequest, args []string return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644) } +func buildTrimArgs(req TrimRequest) []string { + return []string{ + "trim", + "--input-file", req.InputTranscriptPath, + "--output-file", req.OutputTrimmedPath, + "--keep", req.KeepSelector, + } +} + +func writeTrimInvocationConfig(req TrimRequest, args []string, binary string, timeout time.Duration) error { + payload := map[string]any{ + "schema": "seriatim.generated.v1", + "command": "trim", + "binary": binary, + "args": args, + "timeout": timeout.String(), + "input_path": req.InputTranscriptPath, + "output_path": req.OutputTrimmedPath, + "keep_selector": req.KeepSelector, + } + return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644) +} + func validateJSONFile(path string) error { data, err := os.ReadFile(path) if err != nil { @@ -272,3 +379,24 @@ func validateJSONFile(path string) error { } return nil } + +func validateJSONFileWithSegments(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read file: %w", err) + } + + var payload map[string]any + if err := json.Unmarshal(data, &payload); err != nil { + return fmt.Errorf("parse json: %w", err) + } + + segments, ok := payload["segments"] + if !ok { + return fmt.Errorf("top-level segments is required") + } + if _, ok := segments.([]any); !ok { + return fmt.Errorf("top-level segments must be an array") + } + return nil +} diff --git a/internal/adapters/seriatim/subprocess_test.go b/internal/adapters/seriatim/subprocess_test.go index a082a59..e71405a 100644 --- a/internal/adapters/seriatim/subprocess_test.go +++ b/internal/adapters/seriatim/subprocess_test.go @@ -209,6 +209,172 @@ func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) { } } +func TestSubprocessRunnerTrimSuccessInvocationAndProvenance(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("helper wrapper script uses /bin/sh") + } + + t.Setenv("GO_WANT_SERIATIM_HELPER", "1") + t.Setenv("SERIATIM_HELPER_MODE", "trim_success") + recordPath := filepath.Join(t.TempDir(), "record.json") + t.Setenv("SERIATIM_HELPER_RECORD_PATH", recordPath) + + wrapper := writeHelperWrapper(t) + runner := mustRunner(t, wrapper, false) + req := trimReqForTest(t) + + res, err := runner.Trim(context.Background(), req) + if err != nil { + t.Fatalf("Trim() error = %v", err) + } + if res.OutputTrimmedPath != req.OutputTrimmedPath { + t.Fatalf("OutputTrimmedPath = %q, want %q", res.OutputTrimmedPath, req.OutputTrimmedPath) + } + if res.KeepSelector != req.KeepSelector { + t.Fatalf("KeepSelector = %q, want %q", res.KeepSelector, req.KeepSelector) + } + if res.InvokedBinary != wrapper { + t.Fatalf("InvokedBinary = %q, want %q", res.InvokedBinary, wrapper) + } + if res.ExitCode != 0 { + t.Fatalf("ExitCode = %d, want 0", res.ExitCode) + } + if res.Duration <= 0 { + t.Fatalf("Duration = %s, want >0", res.Duration) + } + if res.Metadata == nil || res.Metadata["adapter"] != "seriatim_subprocess" { + t.Fatalf("Metadata = %#v, want adapter marker", res.Metadata) + } + + assertJSONFile(t, req.OutputTrimmedPath) + if _, err := os.Stat(req.StdoutLogPath); err != nil { + t.Fatalf("stdout log missing: %v", err) + } + if _, err := os.Stat(req.StderrLogPath); err != nil { + t.Fatalf("stderr log missing: %v", err) + } + if _, err := os.Stat(req.GeneratedConfigPath); err != nil { + t.Fatalf("generated config missing: %v", err) + } + + stdoutData, err := os.ReadFile(req.StdoutLogPath) + if err != nil { + t.Fatalf("read stdout log: %v", err) + } + if !strings.Contains(string(stdoutData), "seriatim helper trim stdout") { + t.Fatalf("stdout log = %q, want trim helper stdout marker", string(stdoutData)) + } + stderrData, err := os.ReadFile(req.StderrLogPath) + if err != nil { + t.Fatalf("read stderr log: %v", err) + } + if !strings.Contains(string(stderrData), "seriatim helper trim stderr") { + t.Fatalf("stderr log = %q, want trim helper stderr marker", string(stderrData)) + } + + cfgData, err := os.ReadFile(req.GeneratedConfigPath) + if err != nil { + t.Fatalf("read generated config: %v", err) + } + cfgText := string(cfgData) + if !strings.Contains(cfgText, "command: trim") { + t.Fatalf("generated config = %q, want command: trim", cfgText) + } + if !strings.Contains(cfgText, "keep_selector: 5-12") { + t.Fatalf("generated config = %q, want keep selector", cfgText) + } + + rec := readHelperRecord(t, recordPath) + wantArgs := []string{ + "trim", + "--input-file", req.InputTranscriptPath, + "--output-file", req.OutputTrimmedPath, + "--keep", req.KeepSelector, + } + if strings.Join(rec.Args, "\n") != strings.Join(wantArgs, "\n") { + t.Fatalf("args = %#v, want %#v", rec.Args, wantArgs) + } +} + +func TestSubprocessRunnerTrimSubprocessFailure(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("helper wrapper script uses /bin/sh") + } + t.Setenv("GO_WANT_SERIATIM_HELPER", "1") + t.Setenv("SERIATIM_HELPER_MODE", "fail") + t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) + + runner := mustRunner(t, writeHelperWrapper(t), false) + req := trimReqForTest(t) + _, err := runner.Trim(context.Background(), req) + if err == nil { + t.Fatal("Trim() error = nil, want non-nil") + } + if !strings.Contains(err.Error(), "run seriatim trim") { + t.Fatalf("error = %q, want subprocess context", err.Error()) + } + if !strings.Contains(err.Error(), "exit code") { + t.Fatalf("error = %q, want exit code context", err.Error()) + } +} + +func TestSubprocessRunnerTrimMissingOutputFails(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("helper wrapper script uses /bin/sh") + } + t.Setenv("GO_WANT_SERIATIM_HELPER", "1") + t.Setenv("SERIATIM_HELPER_MODE", "missing_output") + t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) + + runner := mustRunner(t, writeHelperWrapper(t), false) + req := trimReqForTest(t) + _, err := runner.Trim(context.Background(), req) + if err == nil { + t.Fatal("Trim() error = nil, want non-nil") + } + if !strings.Contains(err.Error(), "validate seriatim trimmed output") { + t.Fatalf("error = %q, want output validation context", err.Error()) + } +} + +func TestSubprocessRunnerTrimInvalidOutputJSONFails(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("helper wrapper script uses /bin/sh") + } + t.Setenv("GO_WANT_SERIATIM_HELPER", "1") + t.Setenv("SERIATIM_HELPER_MODE", "invalid_output") + t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) + + runner := mustRunner(t, writeHelperWrapper(t), false) + req := trimReqForTest(t) + _, err := runner.Trim(context.Background(), req) + if err == nil { + t.Fatal("Trim() error = nil, want non-nil") + } + if !strings.Contains(err.Error(), "parse json") { + t.Fatalf("error = %q, want parse json context", err.Error()) + } +} + +func TestSubprocessRunnerTrimOutputMissingSegmentsFails(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("helper wrapper script uses /bin/sh") + } + t.Setenv("GO_WANT_SERIATIM_HELPER", "1") + t.Setenv("SERIATIM_HELPER_MODE", "trim_missing_segments") + t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) + + runner := mustRunner(t, writeHelperWrapper(t), false) + req := trimReqForTest(t) + _, err := runner.Trim(context.Background(), req) + if err == nil { + t.Fatal("Trim() error = nil, want non-nil") + } + if !strings.Contains(err.Error(), "top-level segments is required") { + t.Fatalf("error = %q, want missing segments context", err.Error()) + } +} + func TestSubprocessRunnerConstructorValidation(t *testing.T) { _, err := NewSubprocessRunnerFromConfigValues("", "10m", "seriatim-intermediate", nil, true, EnvConfig{}) if err == nil { @@ -250,14 +416,14 @@ func TestSeriatimSubprocessHelper(t *testing.T) { _, _ = os.Stderr.WriteString("missing -- args separator\n") os.Exit(2) } - mergeArgs := args[start:] + cmdArgs := args[start:] - outputPath := flagValue(mergeArgs, "--output-file") - reportPath := flagValue(mergeArgs, "--report-file") + outputPath := flagValue(cmdArgs, "--output-file") + reportPath := flagValue(cmdArgs, "--report-file") recordPath := os.Getenv("SERIATIM_HELPER_RECORD_PATH") if strings.TrimSpace(recordPath) != "" { rec := helperRecord{ - Args: mergeArgs, + Args: cmdArgs, Env: map[string]string{ "SERIATIM_OVERLAP_WORD_RUN_GAP": os.Getenv("SERIATIM_OVERLAP_WORD_RUN_GAP"), "SERIATIM_OVERLAP_WORD_RUN_REORDER_WINDOW": os.Getenv("SERIATIM_OVERLAP_WORD_RUN_REORDER_WINDOW"), @@ -280,6 +446,11 @@ func TestSeriatimSubprocessHelper(t *testing.T) { _, _ = os.Stdout.WriteString("seriatim helper success stdout\n") _, _ = os.Stderr.WriteString("seriatim helper success stderr\n") os.Exit(0) + case "trim_success": + writeSeriatimHelperFile(outputPath, `{"schema":"seriatim.intermediate.v1","segments":[]}`) + _, _ = os.Stdout.WriteString("seriatim helper trim stdout\n") + _, _ = os.Stderr.WriteString("seriatim helper trim stderr\n") + os.Exit(0) case "fail": _, _ = os.Stderr.WriteString("seriatim helper failure\n") os.Exit(9) @@ -300,6 +471,9 @@ func TestSeriatimSubprocessHelper(t *testing.T) { writeSeriatimHelperFile(reportPath, `not-json`) } os.Exit(0) + case "trim_missing_segments": + writeSeriatimHelperFile(outputPath, `{"schema":"seriatim.intermediate.v1"}`) + os.Exit(0) default: _, _ = os.Stderr.WriteString(fmt.Sprintf("unknown helper mode %q\n", mode)) os.Exit(2) @@ -340,6 +514,21 @@ func mergeReqForTest(t *testing.T, withReport bool) MergeRequest { return req } +func trimReqForTest(t *testing.T) TrimRequest { + t.Helper() + dir := t.TempDir() + input := filepath.Join(dir, "processed.json") + writeSeriatimFile(t, input, `{"schema":"seriatim.intermediate.v1","segments":[]}`) + return TrimRequest{ + InputTranscriptPath: input, + OutputTrimmedPath: filepath.Join(dir, "trimmed.json"), + KeepSelector: "5-12", + GeneratedConfigPath: filepath.Join(dir, "seriatim.trim.generated.yml"), + StdoutLogPath: filepath.Join(dir, "seriatim.trim.stdout.log"), + StderrLogPath: filepath.Join(dir, "seriatim.trim.stderr.log"), + } +} + func mustRunner(t *testing.T, binary string, report bool) *SubprocessRunner { t.Helper() coalesce := 3.0