diff --git a/internal/framework/contracts/composition_test.go b/internal/framework/contracts/composition_test.go new file mode 100644 index 0000000..49842c3 --- /dev/null +++ b/internal/framework/contracts/composition_test.go @@ -0,0 +1,150 @@ +package contracts_test + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "gitea.maximumdirect.net/eric/notarius/internal/core/artifacts" + "gitea.maximumdirect.net/eric/notarius/internal/core/source" + "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" +) + +var _ contracts.InputAdapter = compositionAdapter{} +var _ contracts.Extractor = compositionExtractor{} +var _ contracts.Validator = compositionValidator{} + +func TestContractsComposeAcrossPackages(t *testing.T) { + ctx := context.Background() + adapter := compositionAdapter{} + extractor := compositionExtractor{} + validator := compositionValidator{} + + doc, err := adapter.Parse(ctx, contracts.ParseRequest{SourceID: "source-1"}) + if err != nil { + t.Fatalf("Parse() error = %v, want nil", err) + } + if err := source.ValidateDocument(doc); err != nil { + t.Fatalf("ValidateDocument() error = %v, want nil", err) + } + + extraction, err := extractor.Extract(ctx, contracts.ExtractionRequest{Source: doc}) + if err != nil { + t.Fatalf("Extract() error = %v, want nil", err) + } + if len(extraction.Candidates) != 1 { + t.Fatalf("len(Candidates) = %d, want 1", len(extraction.Candidates)) + } + + candidate := extraction.Candidates[0] + for _, ref := range candidate.SourceRefs { + if err := source.ValidateRef(doc, ref); err != nil { + t.Fatalf("ValidateRef() error = %v, want nil", err) + } + } + + validation, err := validator.Validate(ctx, contracts.ValidationRequest{ + Source: doc, + Candidates: extraction.Candidates, + }) + if err != nil { + t.Fatalf("Validate() error = %v, want nil", err) + } + if len(validation.Decisions) != 1 { + t.Fatalf("len(Decisions) = %d, want 1", len(validation.Decisions)) + } + + decision := validation.Decisions[0] + if !decision.Approved { + t.Fatal("Approved = false, want true") + } + if decision.CandidateIndex != candidate.Index { + t.Fatalf("CandidateIndex = %d, want %d", decision.CandidateIndex, candidate.Index) + } +} + +type compositionAdapter struct{} + +func (adapter compositionAdapter) Key() string { + return "generic-input" +} + +func (adapter compositionAdapter) Parse(ctx context.Context, req contracts.ParseRequest) (*source.SourceDocument, error) { + return &source.SourceDocument{ + ID: req.SourceID, + Kind: "document", + Format: "text/plain", + Digest: "sha256:abc123", + Units: []source.SourceUnit{ + {ID: "u1", Kind: "unit", Text: "First source unit."}, + {ID: "u2", Kind: "unit", Text: "Second source unit."}, + }, + }, nil +} + +type compositionExtractor struct{} + +func (extractor compositionExtractor) Key() string { + return "generic-extractor" +} + +func (extractor compositionExtractor) ArtifactType() string { + return "generic-artifact" +} + +func (extractor compositionExtractor) SchemaVersion() string { + return "v1" +} + +func (extractor compositionExtractor) Validators() []contracts.Validator { + return []contracts.Validator{compositionValidator{}} +} + +func (extractor compositionExtractor) Extract(ctx context.Context, req contracts.ExtractionRequest) (contracts.ExtractionResult, error) { + if req.Source == nil { + return contracts.ExtractionResult{}, errors.New("source document is required") + } + + return contracts.ExtractionResult{ + Candidates: []artifacts.Candidate{ + { + Index: 0, + ExtractorKey: extractor.Key(), + ArtifactType: extractor.ArtifactType(), + SchemaVersion: extractor.SchemaVersion(), + Payload: json.RawMessage(`{"value":"example"}`), + SourceRefs: []source.SourceRef{ + { + SourceID: req.Source.ID, + StartUnitID: req.Source.Units[0].ID, + EndUnitID: req.Source.Units[1].ID, + }, + }, + }, + }, + }, nil +} + +type compositionValidator struct{} + +func (validator compositionValidator) Name() string { + return "generic-validator" +} + +func (validator compositionValidator) Validate(ctx context.Context, req contracts.ValidationRequest) (contracts.ValidationResult, error) { + decisions := make([]contracts.ValidationDecision, 0, len(req.Candidates)) + for _, candidate := range req.Candidates { + decisions = append(decisions, contracts.ValidationDecision{ + CandidateIndex: candidate.Index, + Approved: true, + ReasonCode: "accepted", + Message: "candidate accepted", + }) + } + + return contracts.ValidationResult{ + ValidatorName: validator.Name(), + Decisions: decisions, + }, nil +}