Serialize typed artifacts at durable boundaries

This commit is contained in:
2026-07-17 07:33:49 +00:00
parent 66de1a5520
commit 814fcdc6ba
23 changed files with 624 additions and 159 deletions

View File

@@ -42,7 +42,7 @@ func TestModuleSpecAndRegister(t *testing.T) {
func TestEncodeReturnsLogicalFilesForNormalizedOutputs(t *testing.T) {
req := contracts.OutputRequest{
Manifest: artifacts.RunManifest{RunID: "run-1", PipelineID: "pipeline-1"},
NormalizeOutputs: []contracts.NormalizeOutput{
NormalizeOutputs: []contracts.SerializedOutput{
normalizeOutput("spells", `{"spell_casts":[{"spell":"Cure Wounds"}]}`),
normalizeOutput("notes/items", `{"items":[{"name":"Torch"}]}`),
},
@@ -234,7 +234,7 @@ func TestEncodeIncludesManifestRawOutputProvenance(t *testing.T) {
func TestEncodeRejectsLaneIDWithoutSafeFileName(t *testing.T) {
_, err := New().Encode(context.Background(), contracts.OutputRequest{
NormalizeOutputs: []contracts.NormalizeOutput{normalizeOutput("///", `{"value":true}`)},
NormalizeOutputs: []contracts.SerializedOutput{normalizeOutput("///", `{"value":true}`)},
})
if err == nil {
t.Fatal("Encode() error = nil, want unsafe lane id error")
@@ -246,7 +246,7 @@ func TestEncodeRejectsLaneIDWithoutSafeFileName(t *testing.T) {
func TestEncodeSanitizesParentPathSequences(t *testing.T) {
result, err := New().Encode(context.Background(), contracts.OutputRequest{
NormalizeOutputs: []contracts.NormalizeOutput{normalizeOutput("dnd..spell.", `{"value":true}`)},
NormalizeOutputs: []contracts.SerializedOutput{normalizeOutput("dnd..spell.", `{"value":true}`)},
})
if err != nil {
t.Fatalf("Encode() error = %v, want nil", err)
@@ -260,7 +260,7 @@ func TestEncodeSanitizesParentPathSequences(t *testing.T) {
func TestEncodeRejectsInvalidJSONAndUnsupportedMediaTypes(t *testing.T) {
tests := []struct {
name string
output contracts.NormalizeOutput
output contracts.SerializedOutput
want string
}{
{
@@ -270,9 +270,9 @@ func TestEncodeRejectsInvalidJSONAndUnsupportedMediaTypes(t *testing.T) {
},
{
name: "unsupported media type",
output: func() contracts.NormalizeOutput {
output: func() contracts.SerializedOutput {
output := normalizeOutput("spells", `{"spell_casts":[]}`)
output.Payload.MediaType = "text/plain"
output.Artifact.MediaType = "text/plain"
return output
}(),
want: "unsupported media type",
@@ -282,7 +282,7 @@ func TestEncodeRejectsInvalidJSONAndUnsupportedMediaTypes(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := New().Encode(context.Background(), contracts.OutputRequest{
NormalizeOutputs: []contracts.NormalizeOutput{test.output},
NormalizeOutputs: []contracts.SerializedOutput{test.output},
})
if err == nil {
t.Fatal("Encode() error = nil, want error")
@@ -296,7 +296,7 @@ func TestEncodeRejectsInvalidJSONAndUnsupportedMediaTypes(t *testing.T) {
func TestEncodeRejectsSanitizedFilenameCollisions(t *testing.T) {
_, err := New().Encode(context.Background(), contracts.OutputRequest{
NormalizeOutputs: []contracts.NormalizeOutput{
NormalizeOutputs: []contracts.SerializedOutput{
normalizeOutput("a/b", `{"value":"slash"}`),
normalizeOutput("a?b", `{"value":"question"}`),
},
@@ -312,7 +312,7 @@ func TestEncodeRejectsSanitizedFilenameCollisions(t *testing.T) {
func TestEncodeDoesNotMutateInputs(t *testing.T) {
req := contracts.OutputRequest{
Manifest: artifacts.RunManifest{RunID: "run-1"},
NormalizeOutputs: []contracts.NormalizeOutput{
NormalizeOutputs: []contracts.SerializedOutput{
normalizeOutput("spells", `{"name":"original"}`),
},
Rejected: []contracts.RejectedOutput{
@@ -331,8 +331,8 @@ func TestEncodeDoesNotMutateInputs(t *testing.T) {
t.Fatalf("request mutated:\nbefore: %s\nafter: %s", before, after)
}
req.NormalizeOutputs[0].Payload.Content[0] = '['
req.NormalizeOutputs[0].Payload.Metadata["name"] = "changed"
req.NormalizeOutputs[0].Artifact.Content[0] = '['
req.NormalizeOutputs[0].Artifact.Metadata["name"] = "changed"
req.Rejected[0].Message = "changed"
req.Warnings[0].Message = "changed"
@@ -348,7 +348,7 @@ func TestEncodeDoesNotMutateInputs(t *testing.T) {
func TestOutputFilesDoNotContainWarnings(t *testing.T) {
result, err := New().Encode(context.Background(), contracts.OutputRequest{
NormalizeOutputs: []contracts.NormalizeOutput{normalizeOutput("spells", `{"spell":"Shield"}`)},
NormalizeOutputs: []contracts.SerializedOutput{normalizeOutput("spells", `{"spell":"Shield"}`)},
Warnings: []contracts.Warning{
{ReasonCode: "pipeline-warning", Message: "warning"},
},
@@ -363,17 +363,16 @@ func TestOutputFilesDoNotContainWarnings(t *testing.T) {
}
}
func normalizeOutput(laneID string, content string) contracts.NormalizeOutput {
return contracts.NormalizeOutput{
func normalizeOutput(laneID string, content string) contracts.SerializedOutput {
return contracts.SerializedOutput{
LaneID: laneID,
NormalizerKey: "noop",
SourceID: "source-1",
Schema: contracts.ResponseSchema{
Artifact: contracts.SerializedArtifact{Schema: contracts.ArtifactSchema{
ID: "schema-id",
Name: "schema-name",
Version: "v1",
},
Payload: contracts.RawPayload{
Content: []byte(content),
MediaType: contentTypeJSON,
Metadata: map[string]any{"name": laneID},