Clean up MVP output handling

This commit is contained in:
2026-07-03 21:26:05 -05:00
parent 5424aae3de
commit 4db7805a95
10 changed files with 83 additions and 62 deletions

View File

@@ -214,9 +214,6 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
}
runOutputDir := filepath.Join(outputRoot(*outputDir), runDir.RunID())
if err := writeOutputFiles(runOutputDir, output.OutputFiles); err != nil {
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, err)
}
if err := runDir.WriteRunManifest(output.Manifest); err != nil {
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics run manifest: %w", err))
}
@@ -235,6 +232,9 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
}); err != nil {
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics run report: %w", err))
}
if err := writeOutputFiles(runOutputDir, output.OutputFiles); err != nil {
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, err)
}
if err := runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
RetentionMode: cfg.Diagnostics.Retention,
RunSucceeded: true,

View File

@@ -773,6 +773,40 @@ func TestRunPipelineRejectsUnsafeOutputFileName(t *testing.T) {
if got := string(readFile(t, filepath.Join(runDir, diagnostics.ArtifactErrorLog))); !strings.Contains(got, "output file name") {
t.Fatalf("error log = %q, want unsafe output file error", got)
}
if _, err := os.Stat(filepath.Join(runDir, diagnostics.ArtifactRunManifest)); err != nil {
t.Fatalf("expected diagnostics manifest after unsafe output file failure: %v", err)
}
}
func TestRunPipelineWritesDiagnosticsArtifactsWhenDurableOutputWriteFails(t *testing.T) {
diagnosticsDir := t.TempDir()
configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", diagnosticsDir, "always"))
inputPath := writeSeriatimInput(t)
outputRootFile := writeFile(t, "not-a-directory", "occupied")
var stdout bytes.Buffer
var stderr bytes.Buffer
code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputRootFile}, &stdout, &stderr, Options{
LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil),
})
if code != 1 {
t.Fatalf("RunWithOptions() code = %d, want 1", code)
}
if !strings.Contains(stderr.String(), "create output directory") {
t.Fatalf("stderr = %q, want output directory error", stderr.String())
}
runDir := onlyChildDir(t, diagnosticsDir)
for _, name := range []string{
diagnostics.ArtifactRunManifest,
diagnostics.ArtifactWarnings,
diagnostics.ArtifactRunReport,
diagnostics.ArtifactErrorLog,
} {
if _, err := os.Stat(filepath.Join(runDir, name)); err != nil {
t.Fatalf("expected diagnostics artifact %q after durable output write failure: %v", name, err)
}
}
}
func TestRunPipelineWritesDiagnosticsArtifactsOnSuccess(t *testing.T) {

View File

@@ -189,12 +189,8 @@ type OutputFile struct {
}
type OutputResult struct {
Files []OutputFile `json:"files,omitempty"`
// Bytes is the legacy single-output payload. New encoders should return Files.
Bytes []byte `json:"-"`
// ContentType is the legacy single-output content type. New encoders should return Files.
ContentType string `json:"content_type,omitempty"`
Warnings []Warning `json:"warnings,omitempty"`
Files []OutputFile `json:"files,omitempty"`
Warnings []Warning `json:"warnings,omitempty"`
}
type OutputEncoder interface {

View File

@@ -193,7 +193,11 @@ func (output integrationOutput) Key() string {
}
func (output integrationOutput) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
return contracts.OutputResult{Bytes: []byte(`{}`), ContentType: "application/json"}, nil
return contracts.OutputResult{
Files: []contracts.OutputFile{
{Name: "output.json", ContentType: "application/json", Bytes: []byte(`{}`)},
},
}, nil
}
type integrationValidator struct {

View File

@@ -49,10 +49,6 @@ type RunOutput struct {
Rejected []artifacts.RejectedArtifact `json:"rejected,omitempty"`
Warnings []contracts.Warning `json:"warnings,omitempty"`
OutputFiles []contracts.OutputFile `json:"-"`
// EncodedOutput is the legacy single-output payload. New callers should use OutputFiles.
EncodedOutput []byte `json:"-"`
// ContentType is the legacy single-output content type. New callers should use OutputFiles.
ContentType string `json:"content_type,omitempty"`
}
func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
@@ -143,10 +139,6 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
return failOutput(output), fmt.Errorf("validate output files from encoder %q: %w", encoder.Key(), err)
}
output.OutputFiles = files
if len(encoded.Files) == 0 {
output.EncodedOutput = append([]byte(nil), encoded.Bytes...)
output.ContentType = encoded.ContentType
}
return output, nil
}
@@ -425,19 +417,8 @@ func manifestMetadataKey(module any) string {
}
func outputFilesFromResult(result contracts.OutputResult) ([]contracts.OutputFile, error) {
files := result.Files
if len(files) == 0 && len(result.Bytes) > 0 {
files = []contracts.OutputFile{
{
Name: "output",
ContentType: result.ContentType,
Bytes: result.Bytes,
},
}
}
out := make([]contracts.OutputFile, 0, len(files))
for _, file := range files {
out := make([]contracts.OutputFile, 0, len(result.Files))
for _, file := range result.Files {
if err := validateOutputFileName(file.Name); err != nil {
return nil, err
}

View File

@@ -29,13 +29,11 @@ func TestNewAndDataTypes(t *testing.T) {
Metadata: map[string]any{"request": "test"},
}
output := RunOutput{
Manifest: artifacts.RunManifest{PipelineID: "pipeline-1"},
Approved: []artifacts.Artifact{{ExtractorKey: "extract-alpha"}},
Rejected: []artifacts.RejectedArtifact{{ValidatorName: "validator"}},
Warnings: []contracts.Warning{{ReasonCode: "note", Message: "message"}},
OutputFiles: []contracts.OutputFile{{Name: "artifacts/generic.json", ContentType: "application/json", Bytes: []byte(`{}`)}},
EncodedOutput: []byte(`{}`),
ContentType: "application/json",
Manifest: artifacts.RunManifest{PipelineID: "pipeline-1"},
Approved: []artifacts.Artifact{{ExtractorKey: "extract-alpha"}},
Rejected: []artifacts.RejectedArtifact{{ValidatorName: "validator"}},
Warnings: []contracts.Warning{{ReasonCode: "note", Message: "message"}},
OutputFiles: []contracts.OutputFile{{Name: "artifacts/generic.json", ContentType: "application/json", Bytes: []byte(`{}`)}},
}
if input.Pipeline.ID != "pipeline-1" || input.SourceID != "source-1" {
@@ -1140,13 +1138,11 @@ func (validator *runnerValidator) Validate(ctx context.Context, req contracts.Va
}
type runnerOutputEncoder struct {
key string
files []contracts.OutputFile
bytes []byte
contentType string
warnings []contracts.Warning
err error
requests []contracts.OutputRequest
key string
files []contracts.OutputFile
warnings []contracts.Warning
err error
requests []contracts.OutputRequest
}
func (encoder *runnerOutputEncoder) Key() string {
@@ -1156,10 +1152,8 @@ func (encoder *runnerOutputEncoder) Key() string {
func (encoder *runnerOutputEncoder) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
encoder.requests = append(encoder.requests, req)
return contracts.OutputResult{
Files: encoder.files,
Bytes: encoder.bytes,
ContentType: encoder.contentType,
Warnings: encoder.warnings,
Files: encoder.files,
Warnings: encoder.warnings,
}, encoder.err
}

View File

@@ -36,10 +36,13 @@ func TestWalkingSkeletonFixture(t *testing.T) {
if err != nil {
t.Fatalf("Run() error = %v, want nil", err)
}
if output.ContentType != "application/json" {
t.Fatalf("ContentType = %q, want application/json", output.ContentType)
if len(output.OutputFiles) != 1 {
t.Fatalf("len(OutputFiles) = %d, want 1", len(output.OutputFiles))
}
assertStructuralJSONEqual(t, output.EncodedOutput, expectedBytes)
if output.OutputFiles[0].ContentType != "application/json" {
t.Fatalf("ContentType = %q, want application/json", output.OutputFiles[0].ContentType)
}
assertStructuralJSONEqual(t, output.OutputFiles[0].Bytes, expectedBytes)
if llmClient.calls != 2 {
t.Fatalf("LLM calls = %d, want chunk count 2", llmClient.calls)
}
@@ -356,8 +359,9 @@ func (output walkingSkeletonOutput) Encode(ctx context.Context, req contracts.Ou
return contracts.OutputResult{}, err
}
return contracts.OutputResult{
Bytes: encoded,
ContentType: "application/json",
Files: []contracts.OutputFile{
{Name: "output.json", ContentType: "application/json", Bytes: encoded},
},
}, nil
}

View File

@@ -257,8 +257,9 @@ func (dndSpellsOutput) Key() string {
func (dndSpellsOutput) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
return contracts.OutputResult{
Bytes: []byte(`{"encoded":true}`),
ContentType: "application/json",
Files: []contracts.OutputFile{
{Name: "output.json", ContentType: "application/json", Bytes: []byte(`{"encoded":true}`)},
},
}, nil
}

View File

@@ -102,8 +102,11 @@ func TestRunnerProcessesSeriatimInputWithDNDSpellsExtractor(t *testing.T) {
extractorMetadata["response_schema_name"] != ResponseSchemaName {
t.Fatalf("extractor metadata = %#v, want prompt/schema identifiers", extractorMetadata)
}
if output.ContentType != "application/json" {
t.Fatalf("ContentType = %q, want application/json", output.ContentType)
if len(output.OutputFiles) != 1 {
t.Fatalf("len(OutputFiles) = %d, want 1", len(output.OutputFiles))
}
if output.OutputFiles[0].ContentType != "application/json" {
t.Fatalf("ContentType = %q, want application/json", output.OutputFiles[0].ContentType)
}
}

View File

@@ -63,8 +63,11 @@ func TestRunnerProcessesSeriatimInputWithFakeModules(t *testing.T) {
if extractor.calls != 1 {
t.Fatalf("extractor calls = %d, want 1", extractor.calls)
}
if output.ContentType != "application/json" {
t.Fatalf("ContentType = %q, want application/json", output.ContentType)
if len(output.OutputFiles) != 1 {
t.Fatalf("len(OutputFiles) = %d, want 1", len(output.OutputFiles))
}
if output.OutputFiles[0].ContentType != "application/json" {
t.Fatalf("ContentType = %q, want application/json", output.OutputFiles[0].ContentType)
}
}
@@ -237,8 +240,9 @@ func (runnerSeriatimOutput) Key() string {
func (runnerSeriatimOutput) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
return contracts.OutputResult{
Bytes: []byte(`{"encoded":true}`),
ContentType: "application/json",
Files: []contracts.OutputFile{
{Name: "output.json", ContentType: "application/json", Bytes: []byte(`{"encoded":true}`)},
},
}, nil
}