Add Seriatim normalize adapter support
This commit is contained in:
@@ -48,14 +48,37 @@ func (n *NoopRunner) Trim(ctx context.Context, req TrimRequest) (TrimResult, err
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize returns the requested output path with placeholder metadata.
|
||||||
|
func (n *NoopRunner) Normalize(ctx context.Context, req NormalizeRequest) (NormalizeResult, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return NormalizeResult{}, err
|
||||||
|
}
|
||||||
|
if err := materializeNormalizePlaceholders(req); err != nil {
|
||||||
|
return NormalizeResult{}, err
|
||||||
|
}
|
||||||
|
return NormalizeResult{
|
||||||
|
OutputNormalizedPath: req.OutputNormalizedPath,
|
||||||
|
ReportPath: req.ReportPath,
|
||||||
|
StdoutLogPath: req.StdoutLogPath,
|
||||||
|
StderrLogPath: req.StderrLogPath,
|
||||||
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||||
|
InvokedBinary: "noop",
|
||||||
|
OutputSchema: req.OutputSchema,
|
||||||
|
Metadata: map[string]any{"placeholder": true},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// FakeRunner captures merge requests and returns deterministic responses.
|
// FakeRunner captures merge requests and returns deterministic responses.
|
||||||
type FakeRunner struct {
|
type FakeRunner struct {
|
||||||
Requests []MergeRequest
|
Requests []MergeRequest
|
||||||
Err error
|
Err error
|
||||||
Result MergeResult
|
Result MergeResult
|
||||||
TrimRequests []TrimRequest
|
NormalizeRequests []NormalizeRequest
|
||||||
TrimErr error
|
NormalizeErr error
|
||||||
TrimResult TrimResult
|
NormalizeResult NormalizeResult
|
||||||
|
TrimRequests []TrimRequest
|
||||||
|
TrimErr error
|
||||||
|
TrimResult TrimResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run records request and returns configured response.
|
// Run records request and returns configured response.
|
||||||
@@ -132,6 +155,46 @@ func (f *FakeRunner) Trim(ctx context.Context, req TrimRequest) (TrimResult, err
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize records request and returns configured response.
|
||||||
|
func (f *FakeRunner) Normalize(ctx context.Context, req NormalizeRequest) (NormalizeResult, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return NormalizeResult{}, err
|
||||||
|
}
|
||||||
|
f.NormalizeRequests = append(f.NormalizeRequests, req)
|
||||||
|
if f.NormalizeErr != nil {
|
||||||
|
return NormalizeResult{}, f.NormalizeErr
|
||||||
|
}
|
||||||
|
if err := materializeNormalizePlaceholders(req); err != nil {
|
||||||
|
return NormalizeResult{}, err
|
||||||
|
}
|
||||||
|
res := f.NormalizeResult
|
||||||
|
if res.OutputNormalizedPath == "" {
|
||||||
|
res.OutputNormalizedPath = req.OutputNormalizedPath
|
||||||
|
}
|
||||||
|
if res.ReportPath == "" {
|
||||||
|
res.ReportPath = req.ReportPath
|
||||||
|
}
|
||||||
|
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.OutputSchema == "" {
|
||||||
|
res.OutputSchema = req.OutputSchema
|
||||||
|
}
|
||||||
|
if res.Metadata == nil {
|
||||||
|
res.Metadata = map[string]any{"fake": true}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
func materializePlaceholders(req MergeRequest) error {
|
func materializePlaceholders(req MergeRequest) error {
|
||||||
if req.OutputMergedTranscriptPath != "" {
|
if req.OutputMergedTranscriptPath != "" {
|
||||||
if err := subprocess.WriteFileAtomic(req.OutputMergedTranscriptPath, []byte(`{"schema":"seriatim.intermediate.v1","segments":[]}`), 0o644); err != nil {
|
if err := subprocess.WriteFileAtomic(req.OutputMergedTranscriptPath, []byte(`{"schema":"seriatim.intermediate.v1","segments":[]}`), 0o644); err != nil {
|
||||||
@@ -198,3 +261,43 @@ func materializeTrimPlaceholders(req TrimRequest) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func materializeNormalizePlaceholders(req NormalizeRequest) error {
|
||||||
|
if req.OutputNormalizedPath != "" {
|
||||||
|
if err := subprocess.WriteFileAtomic(req.OutputNormalizedPath, []byte(`{"schema":"seriatim.intermediate.v1","segments":[]}`), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write normalized transcript %q: %w", req.OutputNormalizedPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.GeneratedConfigPath != "" {
|
||||||
|
payload := map[string]any{
|
||||||
|
"schema": "seriatim.generated.v1",
|
||||||
|
"placeholder": true,
|
||||||
|
"command": "normalize",
|
||||||
|
"input_path": req.InputTranscriptPath,
|
||||||
|
"output_path": req.OutputNormalizedPath,
|
||||||
|
"output_schema": req.OutputSchema,
|
||||||
|
}
|
||||||
|
if req.ReportPath != "" {
|
||||||
|
payload["report_path"] = req.ReportPath
|
||||||
|
}
|
||||||
|
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 normalize 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 normalize stderr placeholder\n"), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write stderr log %q: %w", req.StderrLogPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.ReportPath != "" {
|
||||||
|
if err := subprocess.WriteFileAtomic(req.ReportPath, []byte(`{"schema":"seriatim.report.v1","placeholder":true}`), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write report %q: %w", req.ReportPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,3 +99,52 @@ func TestFakeRunnerTrimError(t *testing.T) {
|
|||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFakeRunnerNormalizeCapturesRequestAndReturnsPath(t *testing.T) {
|
||||||
|
fake := &FakeRunner{}
|
||||||
|
dir := t.TempDir()
|
||||||
|
req := NormalizeRequest{
|
||||||
|
GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.normalize.yml"),
|
||||||
|
InputTranscriptPath: filepath.Join(dir, "transcripts", "processed.json"),
|
||||||
|
OutputNormalizedPath: filepath.Join(dir, "transcripts", "normalized.json"),
|
||||||
|
OutputSchema: "seriatim-intermediate",
|
||||||
|
ReportPath: filepath.Join(dir, "artifacts", "seriatim.normalize.report.json"),
|
||||||
|
StdoutLogPath: filepath.Join(dir, "logs", "seriatim.normalize.stdout.log"),
|
||||||
|
StderrLogPath: filepath.Join(dir, "logs", "seriatim.normalize.stderr.log"),
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := fake.Normalize(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Normalize() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(fake.NormalizeRequests) != 1 || fake.NormalizeRequests[0].GeneratedConfigPath == "" {
|
||||||
|
t.Fatalf("normalize requests = %#v, want captured request", fake.NormalizeRequests)
|
||||||
|
}
|
||||||
|
if res.OutputNormalizedPath != req.OutputNormalizedPath {
|
||||||
|
t.Fatalf("normalized path = %q, want %q", res.OutputNormalizedPath, req.OutputNormalizedPath)
|
||||||
|
}
|
||||||
|
if res.OutputSchema != req.OutputSchema {
|
||||||
|
t.Fatalf("output schema = %q, want %q", res.OutputSchema, req.OutputSchema)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgData, err := os.ReadFile(req.GeneratedConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read generated config: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(cfgData), "command: normalize") {
|
||||||
|
t.Fatalf("generated config = %q, want normalize command marker", string(cfgData))
|
||||||
|
}
|
||||||
|
for _, path := range []string{req.StdoutLogPath, req.StderrLogPath, req.ReportPath} {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
t.Fatalf("expected file %q to exist: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFakeRunnerNormalizeError(t *testing.T) {
|
||||||
|
fake := &FakeRunner{NormalizeErr: errors.New("boom")}
|
||||||
|
_, err := fake.Normalize(context.Background(), NormalizeRequest{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package seriatim declares the adapter contract for transcript merge/trim execution.
|
// Package seriatim declares the adapter contract for transcript merge/normalize/trim execution.
|
||||||
package seriatim
|
package seriatim
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -6,9 +6,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Runner is the adapter boundary for seriatim merge/trim invocations.
|
// Runner is the adapter boundary for seriatim merge/normalize/trim invocations.
|
||||||
type Runner interface {
|
type Runner interface {
|
||||||
Run(ctx context.Context, req MergeRequest) (MergeResult, error)
|
Run(ctx context.Context, req MergeRequest) (MergeResult, error)
|
||||||
|
Normalize(ctx context.Context, req NormalizeRequest) (NormalizeResult, error)
|
||||||
Trim(ctx context.Context, req TrimRequest) (TrimResult, error)
|
Trim(ctx context.Context, req TrimRequest) (TrimResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +39,33 @@ type MergeResult struct {
|
|||||||
Metadata map[string]any
|
Metadata map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizeRequest describes a seriatim normalize invocation.
|
||||||
|
type NormalizeRequest struct {
|
||||||
|
Binary string
|
||||||
|
InputTranscriptPath string
|
||||||
|
OutputNormalizedPath string
|
||||||
|
OutputSchema string
|
||||||
|
ReportPath string
|
||||||
|
StdoutLogPath string
|
||||||
|
StderrLogPath string
|
||||||
|
GeneratedConfigPath string
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeResult describes a normalize output.
|
||||||
|
type NormalizeResult struct {
|
||||||
|
OutputNormalizedPath string
|
||||||
|
ReportPath string
|
||||||
|
StdoutLogPath string
|
||||||
|
StderrLogPath string
|
||||||
|
GeneratedConfigPath string
|
||||||
|
ExitCode int
|
||||||
|
Duration time.Duration
|
||||||
|
InvokedBinary string
|
||||||
|
OutputSchema string
|
||||||
|
Metadata map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
// TrimRequest describes a seriatim trim invocation.
|
// TrimRequest describes a seriatim trim invocation.
|
||||||
type TrimRequest struct {
|
type TrimRequest struct {
|
||||||
Binary string
|
Binary string
|
||||||
|
|||||||
@@ -77,10 +77,8 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error)
|
|||||||
if strings.TrimSpace(cfg.OutputSchema) == "" {
|
if strings.TrimSpace(cfg.OutputSchema) == "" {
|
||||||
return nil, fmt.Errorf("seriatim output schema is required")
|
return nil, fmt.Errorf("seriatim output schema is required")
|
||||||
}
|
}
|
||||||
switch cfg.OutputSchema {
|
if err := validateOutputSchema(cfg.OutputSchema); err != nil {
|
||||||
case "seriatim-minimal", "seriatim-intermediate", "seriatim-full":
|
return nil, err
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("seriatim output schema %q is unsupported", cfg.OutputSchema)
|
|
||||||
}
|
}
|
||||||
if cfg.CoalesceGap != nil && *cfg.CoalesceGap < 0 {
|
if cfg.CoalesceGap != nil && *cfg.CoalesceGap < 0 {
|
||||||
return nil, fmt.Errorf("seriatim coalesce gap must be >= 0")
|
return nil, fmt.Errorf("seriatim coalesce gap must be >= 0")
|
||||||
@@ -96,6 +94,15 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateOutputSchema(schema string) error {
|
||||||
|
switch strings.TrimSpace(schema) {
|
||||||
|
case "seriatim-minimal", "seriatim-intermediate", "seriatim-full":
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("seriatim output schema %q is unsupported", schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run executes Seriatim merge with deterministic flags and validates output artifacts.
|
// Run executes Seriatim merge with deterministic flags and validates output artifacts.
|
||||||
func (r *SubprocessRunner) Run(ctx context.Context, req MergeRequest) (MergeResult, error) {
|
func (r *SubprocessRunner) Run(ctx context.Context, req MergeRequest) (MergeResult, error) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
@@ -271,6 +278,112 @@ func (r *SubprocessRunner) Trim(ctx context.Context, req TrimRequest) (TrimResul
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize executes Seriatim normalize with deterministic flags and validates output artifacts.
|
||||||
|
func (r *SubprocessRunner) Normalize(ctx context.Context, req NormalizeRequest) (NormalizeResult, error) {
|
||||||
|
if r == nil {
|
||||||
|
return NormalizeResult{}, fmt.Errorf("seriatim subprocess runner is nil")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.InputTranscriptPath) == "" {
|
||||||
|
return NormalizeResult{}, fmt.Errorf("seriatim normalize input path is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.OutputNormalizedPath) == "" {
|
||||||
|
return NormalizeResult{}, fmt.Errorf("seriatim normalize output path is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
binary := r.binary
|
||||||
|
if strings.TrimSpace(req.Binary) != "" {
|
||||||
|
binary = strings.TrimSpace(req.Binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := r.timeout
|
||||||
|
if req.Timeout < 0 {
|
||||||
|
return NormalizeResult{}, fmt.Errorf("seriatim normalize timeout must be >= 0")
|
||||||
|
}
|
||||||
|
if req.Timeout > 0 {
|
||||||
|
timeout = req.Timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
outputSchema := strings.TrimSpace(req.OutputSchema)
|
||||||
|
if outputSchema == "" {
|
||||||
|
outputSchema = r.outputSchema
|
||||||
|
}
|
||||||
|
if err := validateOutputSchema(outputSchema); err != nil {
|
||||||
|
return NormalizeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
args := buildNormalizeArgs(req, outputSchema)
|
||||||
|
if req.GeneratedConfigPath != "" {
|
||||||
|
if err := writeNormalizeInvocationConfig(req, args, binary, timeout, outputSchema); err != nil {
|
||||||
|
return NormalizeResult{}, fmt.Errorf("write seriatim normalize 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 NormalizeResult{
|
||||||
|
OutputNormalizedPath: req.OutputNormalizedPath,
|
||||||
|
ReportPath: req.ReportPath,
|
||||||
|
StdoutLogPath: req.StdoutLogPath,
|
||||||
|
StderrLogPath: req.StderrLogPath,
|
||||||
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||||
|
ExitCode: runRes.ExitCode,
|
||||||
|
Duration: runRes.Duration,
|
||||||
|
InvokedBinary: binary,
|
||||||
|
OutputSchema: outputSchema,
|
||||||
|
}, fmt.Errorf("run seriatim normalize (binary=%q): %w", binary, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateJSONFileWithSegments(req.OutputNormalizedPath); err != nil {
|
||||||
|
return NormalizeResult{
|
||||||
|
OutputNormalizedPath: req.OutputNormalizedPath,
|
||||||
|
ReportPath: req.ReportPath,
|
||||||
|
StdoutLogPath: req.StdoutLogPath,
|
||||||
|
StderrLogPath: req.StderrLogPath,
|
||||||
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||||
|
ExitCode: runRes.ExitCode,
|
||||||
|
Duration: runRes.Duration,
|
||||||
|
InvokedBinary: binary,
|
||||||
|
OutputSchema: outputSchema,
|
||||||
|
}, fmt.Errorf("validate seriatim normalized output %q: %w", req.OutputNormalizedPath, err)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.ReportPath) != "" {
|
||||||
|
if err := validateJSONFile(req.ReportPath); err != nil {
|
||||||
|
return NormalizeResult{
|
||||||
|
OutputNormalizedPath: req.OutputNormalizedPath,
|
||||||
|
ReportPath: req.ReportPath,
|
||||||
|
StdoutLogPath: req.StdoutLogPath,
|
||||||
|
StderrLogPath: req.StderrLogPath,
|
||||||
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||||
|
ExitCode: runRes.ExitCode,
|
||||||
|
Duration: runRes.Duration,
|
||||||
|
InvokedBinary: binary,
|
||||||
|
OutputSchema: outputSchema,
|
||||||
|
}, fmt.Errorf("validate seriatim normalize report output %q: %w", req.ReportPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NormalizeResult{
|
||||||
|
OutputNormalizedPath: req.OutputNormalizedPath,
|
||||||
|
ReportPath: req.ReportPath,
|
||||||
|
StdoutLogPath: req.StdoutLogPath,
|
||||||
|
StderrLogPath: req.StderrLogPath,
|
||||||
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||||
|
ExitCode: runRes.ExitCode,
|
||||||
|
Duration: runRes.Duration,
|
||||||
|
InvokedBinary: binary,
|
||||||
|
OutputSchema: outputSchema,
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"adapter": "seriatim_subprocess",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *SubprocessRunner) buildMergeArgs(req MergeRequest) []string {
|
func (r *SubprocessRunner) buildMergeArgs(req MergeRequest) []string {
|
||||||
args := []string{"merge"}
|
args := []string{"merge"}
|
||||||
|
|
||||||
@@ -354,6 +467,19 @@ func buildTrimArgs(req TrimRequest) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildNormalizeArgs(req NormalizeRequest, outputSchema string) []string {
|
||||||
|
args := []string{
|
||||||
|
"normalize",
|
||||||
|
"--input-file", req.InputTranscriptPath,
|
||||||
|
"--output-file", req.OutputNormalizedPath,
|
||||||
|
"--output-schema", outputSchema,
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.ReportPath) != "" {
|
||||||
|
args = append(args, "--report-file", req.ReportPath)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
func writeTrimInvocationConfig(req TrimRequest, args []string, binary string, timeout time.Duration) error {
|
func writeTrimInvocationConfig(req TrimRequest, args []string, binary string, timeout time.Duration) error {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"schema": "seriatim.generated.v1",
|
"schema": "seriatim.generated.v1",
|
||||||
@@ -368,6 +494,21 @@ func writeTrimInvocationConfig(req TrimRequest, args []string, binary string, ti
|
|||||||
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644)
|
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeNormalizeInvocationConfig(req NormalizeRequest, args []string, binary string, timeout time.Duration, outputSchema string) error {
|
||||||
|
payload := map[string]any{
|
||||||
|
"schema": "seriatim.generated.v1",
|
||||||
|
"command": "normalize",
|
||||||
|
"binary": binary,
|
||||||
|
"args": args,
|
||||||
|
"timeout": timeout.String(),
|
||||||
|
"input_path": req.InputTranscriptPath,
|
||||||
|
"output_path": req.OutputNormalizedPath,
|
||||||
|
"output_schema": outputSchema,
|
||||||
|
"report_path": req.ReportPath,
|
||||||
|
}
|
||||||
|
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
func validateJSONFile(path string) error {
|
func validateJSONFile(path string) error {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -375,6 +375,200 @@ func TestSubprocessRunnerTrimOutputMissingSegmentsFails(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerNormalizeSuccessInvocationAndProvenance(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", "normalize_success")
|
||||||
|
recordPath := filepath.Join(t.TempDir(), "record.json")
|
||||||
|
t.Setenv("SERIATIM_HELPER_RECORD_PATH", recordPath)
|
||||||
|
|
||||||
|
wrapper := writeHelperWrapper(t)
|
||||||
|
runner := mustRunner(t, wrapper, false)
|
||||||
|
req := normalizeReqForTest(t, true)
|
||||||
|
|
||||||
|
res, err := runner.Normalize(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Normalize() error = %v", err)
|
||||||
|
}
|
||||||
|
if res.OutputNormalizedPath != req.OutputNormalizedPath {
|
||||||
|
t.Fatalf("OutputNormalizedPath = %q, want %q", res.OutputNormalizedPath, req.OutputNormalizedPath)
|
||||||
|
}
|
||||||
|
if res.OutputSchema != req.OutputSchema {
|
||||||
|
t.Fatalf("OutputSchema = %q, want %q", res.OutputSchema, req.OutputSchema)
|
||||||
|
}
|
||||||
|
if res.ReportPath != req.ReportPath {
|
||||||
|
t.Fatalf("ReportPath = %q, want %q", res.ReportPath, req.ReportPath)
|
||||||
|
}
|
||||||
|
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.OutputNormalizedPath)
|
||||||
|
assertJSONFile(t, req.ReportPath)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgData, err := os.ReadFile(req.GeneratedConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read generated config: %v", err)
|
||||||
|
}
|
||||||
|
cfgText := string(cfgData)
|
||||||
|
if !strings.Contains(cfgText, "command: normalize") {
|
||||||
|
t.Fatalf("generated config = %q, want command: normalize", cfgText)
|
||||||
|
}
|
||||||
|
if !strings.Contains(cfgText, "output_schema: seriatim-intermediate") {
|
||||||
|
t.Fatalf("generated config = %q, want output schema", cfgText)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := readHelperRecord(t, recordPath)
|
||||||
|
wantArgs := []string{
|
||||||
|
"normalize",
|
||||||
|
"--input-file", req.InputTranscriptPath,
|
||||||
|
"--output-file", req.OutputNormalizedPath,
|
||||||
|
"--output-schema", req.OutputSchema,
|
||||||
|
"--report-file", req.ReportPath,
|
||||||
|
}
|
||||||
|
if strings.Join(rec.Args, "\n") != strings.Join(wantArgs, "\n") {
|
||||||
|
t.Fatalf("args = %#v, want %#v", rec.Args, wantArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerNormalizeSubprocessFailure(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 := normalizeReqForTest(t, false)
|
||||||
|
_, err := runner.Normalize(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Normalize() error = nil, want non-nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "run seriatim normalize") {
|
||||||
|
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 TestSubprocessRunnerNormalizeMissingOutputFails(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", "normalize_missing_output")
|
||||||
|
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
runner := mustRunner(t, writeHelperWrapper(t), false)
|
||||||
|
req := normalizeReqForTest(t, false)
|
||||||
|
_, err := runner.Normalize(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Normalize() error = nil, want non-nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "validate seriatim normalized output") {
|
||||||
|
t.Fatalf("error = %q, want output validation context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerNormalizeInvalidOutputJSONFails(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", "normalize_invalid_output")
|
||||||
|
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
runner := mustRunner(t, writeHelperWrapper(t), false)
|
||||||
|
req := normalizeReqForTest(t, false)
|
||||||
|
_, err := runner.Normalize(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Normalize() error = nil, want non-nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "parse json") {
|
||||||
|
t.Fatalf("error = %q, want parse json context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerNormalizeOutputMissingSegmentsFails(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", "normalize_missing_segments")
|
||||||
|
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
runner := mustRunner(t, writeHelperWrapper(t), false)
|
||||||
|
req := normalizeReqForTest(t, false)
|
||||||
|
_, err := runner.Normalize(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Normalize() 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 TestSubprocessRunnerNormalizeReportMissingFails(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", "normalize_report_missing")
|
||||||
|
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
runner := mustRunner(t, writeHelperWrapper(t), false)
|
||||||
|
req := normalizeReqForTest(t, true)
|
||||||
|
_, err := runner.Normalize(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Normalize() error = nil, want non-nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "validate seriatim normalize report output") {
|
||||||
|
t.Fatalf("error = %q, want report validation context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerNormalizeInvalidReportJSONFails(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", "normalize_invalid_report")
|
||||||
|
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
runner := mustRunner(t, writeHelperWrapper(t), false)
|
||||||
|
req := normalizeReqForTest(t, true)
|
||||||
|
_, err := runner.Normalize(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Normalize() error = nil, want non-nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "validate seriatim normalize report output") {
|
||||||
|
t.Fatalf("error = %q, want report validation context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSubprocessRunnerConstructorValidation(t *testing.T) {
|
func TestSubprocessRunnerConstructorValidation(t *testing.T) {
|
||||||
_, err := NewSubprocessRunnerFromConfigValues("", "10m", "seriatim-intermediate", nil, true, EnvConfig{})
|
_, err := NewSubprocessRunnerFromConfigValues("", "10m", "seriatim-intermediate", nil, true, EnvConfig{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -451,6 +645,14 @@ func TestSeriatimSubprocessHelper(t *testing.T) {
|
|||||||
_, _ = os.Stdout.WriteString("seriatim helper trim stdout\n")
|
_, _ = os.Stdout.WriteString("seriatim helper trim stdout\n")
|
||||||
_, _ = os.Stderr.WriteString("seriatim helper trim stderr\n")
|
_, _ = os.Stderr.WriteString("seriatim helper trim stderr\n")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
case "normalize_success":
|
||||||
|
writeSeriatimHelperFile(outputPath, `{"schema":"seriatim.intermediate.v1","segments":[]}`)
|
||||||
|
if reportPath != "" {
|
||||||
|
writeSeriatimHelperFile(reportPath, `{"schema":"seriatim.normalize.report.v1","ok":true}`)
|
||||||
|
}
|
||||||
|
_, _ = os.Stdout.WriteString("seriatim helper normalize stdout\n")
|
||||||
|
_, _ = os.Stderr.WriteString("seriatim helper normalize stderr\n")
|
||||||
|
os.Exit(0)
|
||||||
case "fail":
|
case "fail":
|
||||||
_, _ = os.Stderr.WriteString("seriatim helper failure\n")
|
_, _ = os.Stderr.WriteString("seriatim helper failure\n")
|
||||||
os.Exit(9)
|
os.Exit(9)
|
||||||
@@ -459,21 +661,47 @@ func TestSeriatimSubprocessHelper(t *testing.T) {
|
|||||||
writeSeriatimHelperFile(reportPath, `{"report":true}`)
|
writeSeriatimHelperFile(reportPath, `{"report":true}`)
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
case "normalize_missing_output":
|
||||||
|
if reportPath != "" {
|
||||||
|
writeSeriatimHelperFile(reportPath, `{"schema":"seriatim.normalize.report.v1","ok":true}`)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
case "invalid_output":
|
case "invalid_output":
|
||||||
writeSeriatimHelperFile(outputPath, `not-json`)
|
writeSeriatimHelperFile(outputPath, `not-json`)
|
||||||
if reportPath != "" {
|
if reportPath != "" {
|
||||||
writeSeriatimHelperFile(reportPath, `{"report":true}`)
|
writeSeriatimHelperFile(reportPath, `{"report":true}`)
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
case "normalize_invalid_output":
|
||||||
|
writeSeriatimHelperFile(outputPath, `not-json`)
|
||||||
|
if reportPath != "" {
|
||||||
|
writeSeriatimHelperFile(reportPath, `{"schema":"seriatim.normalize.report.v1","ok":true}`)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
case "invalid_report":
|
case "invalid_report":
|
||||||
writeSeriatimHelperFile(outputPath, `{"merged":true}`)
|
writeSeriatimHelperFile(outputPath, `{"merged":true}`)
|
||||||
if reportPath != "" {
|
if reportPath != "" {
|
||||||
writeSeriatimHelperFile(reportPath, `not-json`)
|
writeSeriatimHelperFile(reportPath, `not-json`)
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
case "normalize_invalid_report":
|
||||||
|
writeSeriatimHelperFile(outputPath, `{"schema":"seriatim.intermediate.v1","segments":[]}`)
|
||||||
|
if reportPath != "" {
|
||||||
|
writeSeriatimHelperFile(reportPath, `not-json`)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
case "trim_missing_segments":
|
case "trim_missing_segments":
|
||||||
writeSeriatimHelperFile(outputPath, `{"schema":"seriatim.intermediate.v1"}`)
|
writeSeriatimHelperFile(outputPath, `{"schema":"seriatim.intermediate.v1"}`)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
case "normalize_missing_segments":
|
||||||
|
writeSeriatimHelperFile(outputPath, `{"schema":"seriatim.intermediate.v1"}`)
|
||||||
|
if reportPath != "" {
|
||||||
|
writeSeriatimHelperFile(reportPath, `{"schema":"seriatim.normalize.report.v1","ok":true}`)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
case "normalize_report_missing":
|
||||||
|
writeSeriatimHelperFile(outputPath, `{"schema":"seriatim.intermediate.v1","segments":[]}`)
|
||||||
|
os.Exit(0)
|
||||||
default:
|
default:
|
||||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("unknown helper mode %q\n", mode))
|
_, _ = os.Stderr.WriteString(fmt.Sprintf("unknown helper mode %q\n", mode))
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
@@ -529,6 +757,26 @@ func trimReqForTest(t *testing.T) TrimRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeReqForTest(t *testing.T, withReport bool) NormalizeRequest {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
input := filepath.Join(dir, "processed.json")
|
||||||
|
writeSeriatimFile(t, input, `{"schema":"audita.processed.v1","segments":[]}`)
|
||||||
|
|
||||||
|
req := NormalizeRequest{
|
||||||
|
InputTranscriptPath: input,
|
||||||
|
OutputNormalizedPath: filepath.Join(dir, "normalized.json"),
|
||||||
|
OutputSchema: "seriatim-intermediate",
|
||||||
|
GeneratedConfigPath: filepath.Join(dir, "seriatim.normalize.generated.yml"),
|
||||||
|
StdoutLogPath: filepath.Join(dir, "seriatim.normalize.stdout.log"),
|
||||||
|
StderrLogPath: filepath.Join(dir, "seriatim.normalize.stderr.log"),
|
||||||
|
}
|
||||||
|
if withReport {
|
||||||
|
req.ReportPath = filepath.Join(dir, "seriatim.normalize.report.json")
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
func mustRunner(t *testing.T, binary string, report bool) *SubprocessRunner {
|
func mustRunner(t *testing.T, binary string, report bool) *SubprocessRunner {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
coalesce := 3.0
|
coalesce := 3.0
|
||||||
|
|||||||
Reference in New Issue
Block a user