package integration_test import ( "context" "encoding/json" "fmt" "reflect" "strings" "sync" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/config" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/evidencecontext" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" locationoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationoccurrences" locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry" locationoccurrences "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences" locations "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry" locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry" ) func TestLocationRegistryHandoffProducesOccurrencesAndEvidence(t *testing.T) { registries := productionNPCRegistries(t) resolved := resolveLocationPipeline(t, registries) client := &locationHandoffLLMClient{} output, err := runPreparedPipeline(t, registries, resolved, client, pipeline.RunInput{RawInput: readNPCFixture(t)}) if err != nil { t.Fatalf("Run() error = %v", err) } if len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 2 { t.Fatalf("run outputs = %#v rejected = %#v, want accepted registry and occurrences", output.NormalizeOutputs, output.Rejected) } registryOutput := normalizedLane(t, output, "locations") if registryOutput.Artifact.Kind != dnd.LocationRegistryKind || registryOutput.Artifact.Schema.ID != locationcodec.SchemaID { t.Fatalf("registry envelope = %#v, want durable location artifact", registryOutput) } registry, err := locationcodec.New().Decode(registryOutput.Artifact.Content) if err != nil { t.Fatalf("Decode(registry) error = %v", err) } if len(registry.Locations) != 2 || registry.Locations[0].Name != "Moon Gate" || registry.Locations[1].Name != "Moon Gate" || registry.Locations[0].ID == registry.Locations[1].ID { t.Fatalf("registry = %#v, want same-name locations distinguished by source-derived IDs", registry) } occurrenceOutput := normalizedLane(t, output, "occurrences") if occurrenceOutput.Artifact.Kind != dnd.LocationOccurrenceListKind || occurrenceOutput.Artifact.Schema.ID != locationoccurrencecodec.SchemaID { t.Fatalf("occurrence envelope = %#v, want durable occurrence artifact", occurrenceOutput) } occurrences, err := locationoccurrencecodec.New().Decode(occurrenceOutput.Artifact.Content) if err != nil { t.Fatalf("Decode(occurrences) error = %v", err) } if len(occurrences.Occurrences) != 2 || occurrences.Occurrences[0].LocationID == occurrences.Occurrences[1].LocationID || occurrences.Occurrences[0].LocationID != registry.Locations[0].ID || occurrences.Occurrences[1].LocationID != registry.Locations[1].ID { t.Fatalf("occurrences = %#v, want exact registry ID grounding", occurrences) } for _, occurrence := range occurrences.Occurrences { if occurrence.SourceRefs[0].SourceID != "npc-session" { t.Fatalf("occurrence evidence = %#v, want current source evidence only", occurrence.SourceRefs) } } request := client.requestFor(t, locationoccurrences.PromptID) registryInput := request.Inputs["location_registry"] if registryInput.MediaType != locationcodec.MediaType || strings.Contains(string(registryInput.Content), "source_refs") || strings.Contains(string(registryInput.Content), registry.Locations[0].ID) || strings.Contains(string(registryInput.Content), registry.Locations[1].ID) || !strings.Contains(string(registryInput.Content), "registry_refs") { t.Fatalf("occurrence registry input = %#v, want contextual selector projection", registryInput) } contextArtifact := outputFileContent(t, output.OutputFiles, "evidence-context.json") evidence, err := evidencecontext.New().Decode(contextArtifact) if err != nil { t.Fatalf("Decode(evidence context) error = %v", err) } if actual := locationEvidenceUnitIDs(evidence); !reflect.DeepEqual(actual, []int{1, 2, 3, 4, 5}) { t.Fatalf("evidence context = %#v, want deduplicated registry and occurrence source-unit evidence", evidence) } } func locationEvidenceUnitIDs(document evidencecontext.Document) []int { ids := make([]int, len(document)) for index, unit := range document { ids[index] = unit.ID } return ids } func TestLocationOccurrenceConsumerDoesNotRunAfterRejectedRegistry(t *testing.T) { registries := productionNPCRegistries(t) client := &locationHandoffLLMClient{rejectRegistry: true} _, err := runPreparedPipeline(t, registries, resolveLocationPipeline(t, registries), client, pipeline.RunInput{RawInput: readNPCFixture(t)}) if err == nil { t.Fatal("Run() error = nil, want missing accepted location registry") } if client.requestCount(locationoccurrences.PromptID) != 0 { t.Fatalf("occurrence requests = %d, want none after rejected registry", client.requestCount(locationoccurrences.PromptID)) } } func TestLocationOccurrenceConfigurationRequiresEarlierRegistry(t *testing.T) { registries := productionNPCRegistries(t) cfg, err := configFromYAML(strings.Replace(locationPipelineConfig, "step: identify-locations", "step: track-location-occurrences", 1)) if err != nil { t.Fatal(err) } _, err = cfg.Resolve(config.ResolveInput{PipelineID: "dnd-locations-fixture", Catalog: moduleCatalog(registries)}) if err == nil || !strings.Contains(err.Error(), "earlier step") { t.Fatalf("Resolve() error = %v, want an earlier generated registry requirement", err) } } func TestLocationOccurrenceCheckpointTracksGeneratedRegistry(t *testing.T) { registries := productionNPCRegistries(t) checkpoint := newGeneratedReferenceCheckpointLoader() _, err := runPreparedPipeline(t, registries, resolveLocationPipeline(t, registries), &locationHandoffLLMClient{}, pipeline.RunInput{ RawInput: readNPCFixture(t), Checkpoint: checkpoint, }) if err != nil { t.Fatalf("Run() error = %v", err) } for _, dependency := range checkpoint.extractDependencies("occurrences") { if dependency.Name == "generated-reference:location_registry:0" && strings.HasPrefix(dependency.Value, "sha256:") { return } } t.Fatalf("occurrence checkpoint dependencies = %#v, want generated location registry fingerprint", checkpoint.extractDependencies("occurrences")) } func resolveLocationPipeline(t *testing.T, registries pipeline.Registries) pipeline.ResolvedPipeline { t.Helper() cfg, err := configFromYAML(locationPipelineConfig) if err != nil { t.Fatal(err) } effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-locations-fixture", Catalog: moduleCatalog(registries)}) if err != nil { t.Fatalf("Resolve() error = %v", err) } resolved, warnings, err := pipeline.MaterializeReferences(effective.ResolvedPipeline, moduleCatalog(registries), pipeline.ReferenceMaterializationOptions{}) if err != nil || len(warnings) != 0 { t.Fatalf("MaterializeReferences() error = %v warnings = %#v", err, warnings) } return resolved } func configFromYAML(content string) (config.Config, error) { fileConfig, err := config.ParseFileConfigYAML([]byte(content)) if err != nil { return config.Config{}, err } cfg := config.Default() if err := cfg.ApplyFileConfig(fileConfig); err != nil { return config.Config{}, err } return cfg, nil } const locationPipelineConfig = `version: 4 cache: chunk_plans: mode: bypass checkpoints: {} pipelines: dnd-locations-fixture: input: seriatim output: module: json options: evidence_context: enabled: true lanes: [locations, occurrences] steps: - id: identify-locations artifacts: locations: extract: dnd/location-registry merge: appendorder normalize: dnd/location-registry - id: track-location-occurrences references: location_registry: artifact: step: identify-locations lane: locations artifacts: occurrences: extract: dnd/location-occurrences merge: appendorder normalize: dnd/location-occurrences ` type locationHandoffLLMClient struct { mu sync.Mutex requests []contracts.StructuredCompletionRequest rejectRegistry bool } func (client *locationHandoffLLMClient) CompleteStructured(ctx context.Context, request contracts.StructuredCompletionRequest, output any) (contracts.StructuredCompletionResponse, error) { if err := ctx.Err(); err != nil { return contracts.StructuredCompletionResponse{}, err } client.mu.Lock() client.requests = append(client.requests, cloneStructuredCompletionRequest(request)) client.mu.Unlock() var payload any switch request.PromptID { case locations.PromptID: name := "Moon Gate" if client.rejectRegistry { name = "" } payload = map[string]any{"locations": []any{map[string]any{"name": name, "source_refs": []any{map[string]int{"start_unit_id": 1, "end_unit_id": 1}}}, map[string]any{"name": name, "source_refs": []any{map[string]int{"start_unit_id": 3, "end_unit_id": 3}}}}} case locationnormalize.PromptID: payload = map[string]any{"duplicate_groups": []any{}} case locationoccurrences.PromptID: var projection struct { Locations []struct { Name string `json:"name"` RegistryRefs []struct { StartUnitID int `json:"start_unit_id"` EndUnitID int `json:"end_unit_id"` } `json:"registry_refs"` } `json:"locations"` } if err := json.Unmarshal(request.Inputs["location_registry"].Content, &projection); err != nil { return contracts.StructuredCompletionResponse{}, fmt.Errorf("decode location registry: %w", err) } if len(projection.Locations) != 2 { return contracts.StructuredCompletionResponse{}, fmt.Errorf("generated location registry has %d locations, want 2", len(projection.Locations)) } payload = map[string]any{"occurrences": []any{map[string]any{"name": projection.Locations[0].Name, "registry_refs": projection.Locations[0].RegistryRefs, "kind": "visited", "source_refs": []any{map[string]int{"start_unit_id": 1, "end_unit_id": 1}}}, map[string]any{"name": projection.Locations[1].Name, "registry_refs": projection.Locations[1].RegistryRefs, "kind": "mentioned", "source_refs": []any{map[string]int{"start_unit_id": 3, "end_unit_id": 3}}}}} default: return contracts.StructuredCompletionResponse{}, fmt.Errorf("unexpected prompt %q", request.PromptID) } content, err := json.Marshal(payload) if err != nil { return contracts.StructuredCompletionResponse{}, err } if err := json.Unmarshal(content, output); err != nil { return contracts.StructuredCompletionResponse{}, err } return contracts.StructuredCompletionResponse{Content: content, Provider: "test", Model: "location-handoff"}, nil } func (client *locationHandoffLLMClient) requestFor(t *testing.T, promptID string) contracts.StructuredCompletionRequest { t.Helper() client.mu.Lock() defer client.mu.Unlock() for _, request := range client.requests { if request.PromptID == promptID { return request } } t.Fatalf("requests = %#v, missing %q", client.requests, promptID) return contracts.StructuredCompletionRequest{} } func (client *locationHandoffLLMClient) requestCount(promptID string) int { client.mu.Lock() defer client.mu.Unlock() count := 0 for _, request := range client.requests { if request.PromptID == promptID { count++ } } return count } var _ contracts.StructuredLLMClient = (*locationHandoffLLMClient)(nil)