package artifacts import ( "encoding/json" "strings" "testing" ) func TestRunManifestOmitsEmptyOptionalFields(t *testing.T) { gotJSON, err := json.Marshal(RunManifest{}) if err != nil { t.Fatalf("json.Marshal() error = %v", err) } if string(gotJSON) != "{}" { t.Fatalf("json.Marshal(RunManifest{}) = %s, want {}", gotJSON) } } func TestRunManifestChunkPlanIsAdditiveAndOmitsPlanContent(t *testing.T) { manifest := RunManifest{ChunkPlan: &ChunkPlanManifest{ Mode: "auto", Action: "reused", SourceDigest: "sha256:source", PlanDigest: "sha256:plan", PlanSchemaVersion: "notarius.chunk-plan.v2", RequestedModule: "chunk/current", ProducerInputModule: "input/original", ProducerModule: "chunk/original", }} encoded, err := json.Marshal(manifest) if err != nil { t.Fatal(err) } text := string(encoded) for _, want := range []string{`"chunk_plan"`, `"action":"reused"`, `"requested_module":"chunk/current"`, `"producer_module":"chunk/original"`} { if !strings.Contains(text, want) { t.Fatalf("manifest JSON %s does not contain %s", text, want) } } for _, forbidden := range []string{`"plan"`, `"units"`, `"annotations"`} { if strings.Contains(text, forbidden) { t.Fatalf("manifest JSON contains forbidden field %s: %s", forbidden, text) } } var legacy RunManifest if err := json.Unmarshal([]byte(`{"pipeline_id":"legacy"}`), &legacy); err != nil { t.Fatal(err) } if legacy.PipelineID != "legacy" || legacy.ChunkPlan != nil { t.Fatalf("legacy manifest = %#v", legacy) } } func TestRunManifestIncludesPipelineAndArtifactLaneFields(t *testing.T) { manifest := RunManifest{ PipelineID: "pipeline-1", PipelineDigest: "sha256:abc123", LLMProfiles: []LLMProfileManifest{ { ID: "default", Provider: "promptkit", Model: "model-a", BackendID: "openrouter", ReasoningEffort: "high", }, }, ArtifactLanes: []ArtifactLaneManifest{ { ID: "events", Extractor: "event-extractor", Merger: "appendorder", Normalizer: "noop", Metadata: map[string]any{ "extractor": map[string]any{"prompt_id": "test.prompt"}, }, }, }, ValidatorChains: []ValidatorChainManifest{ { Stage: "extract", LaneID: "events", ModuleKey: "event-extractor", Validators: []ValidatorManifest{ {Key: "grounded", ExecutionClass: "deterministic"}, }, }, }, } gotJSON, err := json.Marshal(manifest) if err != nil { t.Fatalf("json.Marshal() error = %v", err) } var got map[string]any if err := json.Unmarshal(gotJSON, &got); err != nil { t.Fatalf("json.Unmarshal() error = %v", err) } assertHasKeys(t, got, "pipeline_id", "pipeline_digest", "artifact_lanes", "validator_chains", "llm_profiles") profiles, ok := got["llm_profiles"].([]any) if !ok { t.Fatalf("llm_profiles = %#v, want array", got["llm_profiles"]) } if len(profiles) != 1 { t.Fatalf("len(llm_profiles) = %d, want 1", len(profiles)) } profile, ok := profiles[0].(map[string]any) if !ok { t.Fatalf("llm_profiles[0] = %#v, want object", profiles[0]) } assertHasKeys(t, profile, "id", "provider", "model", "backend_id", "reasoning_effort") if profile["provider"] != "promptkit" { t.Fatalf("llm_profiles[0].provider = %#v, want promptkit", profile["provider"]) } if profile["backend_id"] != "openrouter" || profile["reasoning_effort"] != "high" { t.Fatalf("llm_profiles[0] = %#v, want backend and reasoning provenance", profile) } lanes, ok := got["artifact_lanes"].([]any) if !ok { t.Fatalf("artifact_lanes = %#v, want array", got["artifact_lanes"]) } if len(lanes) != 1 { t.Fatalf("len(artifact_lanes) = %d, want 1", len(lanes)) } lane, ok := lanes[0].(map[string]any) if !ok { t.Fatalf("artifact_lanes[0] = %#v, want object", lanes[0]) } assertHasKeys(t, lane, "id", "extractor", "merger", "normalizer", "metadata") chains, ok := got["validator_chains"].([]any) if !ok { t.Fatalf("validator_chains = %#v, want array", got["validator_chains"]) } if len(chains) != 1 { t.Fatalf("len(validator_chains) = %d, want 1", len(chains)) } chain, ok := chains[0].(map[string]any) if !ok { t.Fatalf("validator_chains[0] = %#v, want object", chains[0]) } assertHasKeys(t, chain, "stage", "lane_id", "module_key", "validators") } func TestRunManifestIncludesReferenceProvenance(t *testing.T) { manifest := RunManifest{ References: []ReferenceProvenance{ { Stage: "extract", LaneID: "events", SlotName: "roster", OriginType: "file", OriginURI: "file:///tmp/roster.txt", Digest: "sha256:reference", MediaType: "text/plain; charset=utf-8", SizeBytes: 12, BindingSource: "config", }, }, } gotJSON, err := json.Marshal(manifest) if err != nil { t.Fatalf("json.Marshal() error = %v", err) } var got RunManifest if err := json.Unmarshal(gotJSON, &got); err != nil { t.Fatalf("json.Unmarshal() error = %v", err) } if len(got.References) != 1 { t.Fatalf("len(References) = %d, want 1", len(got.References)) } reference := got.References[0] if reference.Stage != "extract" || reference.LaneID != "events" || reference.SlotName != "roster" || reference.OriginType != "file" || reference.OriginURI != "file:///tmp/roster.txt" { t.Fatalf("reference provenance = %#v, want lane-scoped origin details", reference) } if reference.Digest != "sha256:reference" || reference.MediaType != "text/plain; charset=utf-8" || reference.SizeBytes != 12 || reference.BindingSource != "config" { t.Fatalf("reference provenance = %#v, want digest/media/size/source details", reference) } } func TestRunManifestIncludesTopLevelModuleMetadata(t *testing.T) { manifest := RunManifest{ ModuleMetadata: map[string]map[string]any{ "chunker": { "prompt_id": "dnd.scenes", "prompt_version": "v1", "prompt_sha256": "sha256:abc123", "response_schema_key": "dnd_scenes", "response_schema_name": "dnd_scenes", }, }, } gotJSON, err := json.Marshal(manifest) if err != nil { t.Fatalf("json.Marshal() error = %v", err) } var got map[string]any if err := json.Unmarshal(gotJSON, &got); err != nil { t.Fatalf("json.Unmarshal() error = %v", err) } moduleMetadata, ok := got["module_metadata"].(map[string]any) if !ok { t.Fatalf("module_metadata = %#v, want object", got["module_metadata"]) } assertHasKeys(t, moduleMetadata, "chunker") chunkerMetadata, ok := moduleMetadata["chunker"].(map[string]any) if !ok { t.Fatalf("module_metadata.chunker = %#v, want object", moduleMetadata["chunker"]) } assertHasKeys(t, chunkerMetadata, "prompt_id", "prompt_version", "prompt_sha256", "response_schema_key", "response_schema_name") } func assertHasKeys(t *testing.T, values map[string]any, keys ...string) { t.Helper() for _, key := range keys { if _, ok := values[key]; !ok { t.Fatalf("missing key %q in %#v", key, values) } } }