package usecase import ( "context" "errors" "reflect" "testing" "gitea.maximumdirect.net/eric/promptkit/internal/domain" "gitea.maximumdirect.net/eric/promptkit/internal/promptdef" ) type inspectionPromptRepository struct { definition *domain.PromptDefinition err error calls int id string } type promptMetadataRepository struct { inspection domain.PromptInspection inspectionErr error lookupID string } func (r *inspectionPromptRepository) GetPromptDefinition( _ context.Context, id string, ) (*domain.PromptDefinition, error) { r.calls++ r.id = id if r.err != nil { return nil, r.err } return r.definition, nil } func (r *promptMetadataRepository) GetPromptDefinition(context.Context, string) (*domain.PromptDefinition, error) { return nil, errors.New("full prompt definition should not be requested") } func (r *promptMetadataRepository) GetPromptInspection( _ context.Context, id string, ) (*domain.PromptInspection, error) { r.lookupID = id inspection := r.inspection inspection.Inputs = append([]domain.PromptInput(nil), inspection.Inputs...) return &inspection, r.inspectionErr } func TestRunnerInspectPromptResolvesOneDefinitionWithoutExecutionCollaborators(t *testing.T) { definition := &domain.PromptDefinition{ ID: "normalized.prompt", Version: "1.2.3", DefaultProfile: "not-resolved", Inputs: []domain.PromptInput{ {Name: "document", Required: true, ContentType: "text/plain", Description: "Source document."}, {Name: "audience", ContentType: "text/plain", Description: "Intended reader."}, }, Validation: domain.OutputContract{ Format: domain.FormatJSON, ValidationMode: domain.ValidationJSONSchema, SchemaPath: "schemas/result.json", RepairAttempts: 2, }, } repository := &inspectionPromptRepository{definition: definition} runner := &Runner{promptDefs: repository} inspection, err := runner.InspectPrompt(context.Background(), " prompt-id ") if err != nil { t.Fatalf("inspect prompt: %v", err) } wantHash, err := hashPromptDefinition(definition) if err != nil { t.Fatalf("hash prompt definition: %v", err) } if repository.calls != 1 || repository.id != " prompt-id " { t.Fatalf("prompt lookup=(calls=%d id=%q), want one unchanged ID lookup", repository.calls, repository.id) } if inspection.PromptID != definition.ID || inspection.PromptVersion != definition.Version || inspection.PromptHash != wantHash || inspection.DefaultProfileID != definition.DefaultProfile || !reflect.DeepEqual(inspection.Inputs, definition.Inputs) || inspection.OutputContract != definition.Validation { t.Fatalf("inspection=%#v, want definition metadata", inspection) } inspection.Inputs[0].Name = "changed" second, err := runner.InspectPrompt(context.Background(), " prompt-id ") if err != nil { t.Fatalf("inspect prompt again: %v", err) } if definition.Inputs[0].Name != "document" || second.Inputs[0].Name != "document" { t.Fatalf("inspection input mutation escaped caller result: definition=%#v next=%#v", definition.Inputs, second.Inputs) } } func TestRunnerInspectPromptUsesOwnedMetadataWhenRepositorySupportsIt(t *testing.T) { repository := &promptMetadataRepository{inspection: domain.PromptInspection{ PromptID: "captured.prompt", PromptVersion: "v2", PromptHash: "cached-prompt-hash", DefaultProfileID: "partial-profile", Inputs: []domain.PromptInput{{Name: "document", Required: true, ContentType: "text/plain", Description: "Captured."}}, OutputContract: domain.OutputContract{Format: domain.FormatJSON, ValidationMode: domain.ValidationJSON}, }} runner := &Runner{promptDefs: repository} inspection, err := runner.InspectPrompt(context.Background(), " requested-id ") if err != nil || repository.lookupID != " requested-id " || inspection.PromptID != "captured.prompt" || inspection.PromptVersion != "v2" || inspection.PromptHash != "cached-prompt-hash" || inspection.DefaultProfileID != "partial-profile" || !reflect.DeepEqual(inspection.Inputs, repository.inspection.Inputs) || inspection.OutputContract != repository.inspection.OutputContract { t.Fatalf("metadata inspection=(%+v, %v), lookup=%q", inspection, err, repository.lookupID) } inspection.Inputs[0].Name = "caller mutation" inspection.OutputContract.SchemaPath = "caller mutation" second, err := runner.InspectPrompt(context.Background(), "requested-id") if err != nil || second.Inputs[0].Name != "document" || second.OutputContract.SchemaPath != "" { t.Fatalf("metadata inspection was not caller-owned: (%+v, %v)", second, err) } } func TestRunnerInspectPromptUsesCapturedHashWhenMetadataCapabilityIsAbsent(t *testing.T) { repository := &requirementInspectionPromptRepository{ definition: &domain.PromptDefinition{ ID: "captured.prompt", Version: "v1", Inputs: []domain.PromptInput{{Name: "document"}}, }, } inspection, err := (&Runner{promptDefs: repository}).InspectPrompt(context.Background(), " requested-id ") if err != nil || inspection.PromptID != "captured.prompt" || inspection.PromptVersion != "v1" || inspection.PromptHash != "captured-hash" || inspection.Inputs[0].Name != "document" { t.Fatalf("captured-hash fallback inspection=(%+v, %v)", inspection, err) } } func TestRunnerInspectPromptClassifiesFailuresWithoutRepositoryWorkAfterCancellation(t *testing.T) { t.Run("blank ID", func(t *testing.T) { repository := &inspectionPromptRepository{} _, err := (&Runner{promptDefs: repository}).InspectPrompt(context.Background(), " \t ") if !errors.Is(err, ErrInvalidRequest) || repository.calls != 0 { t.Fatalf("blank inspection=(%v, calls=%d), want invalid request without lookup", err, repository.calls) } }) t.Run("canceled context", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() repository := &inspectionPromptRepository{} _, err := (&Runner{promptDefs: repository}).InspectPrompt(ctx, "prompt") if !errors.Is(err, ErrPromptLoad) || !errors.Is(err, context.Canceled) || repository.calls != 0 { t.Fatalf("canceled inspection=(%v, calls=%d), want prompt load and context identities without lookup", err, repository.calls) } }) t.Run("missing prompt", func(t *testing.T) { repository := &inspectionPromptRepository{err: promptdef.ErrPromptDefinitionNotFound} _, err := (&Runner{promptDefs: repository}).InspectPrompt(context.Background(), "missing") if !errors.Is(err, ErrPromptLoad) || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) { t.Fatalf("missing prompt error=%v, want prompt load and not-found identities", err) } }) t.Run("defensive prompt dependencies", func(t *testing.T) { var nilRunner *Runner cases := []struct { name string runner *Runner }{ {name: "nil runner", runner: nilRunner}, {name: "nil repository", runner: &Runner{}}, {name: "nil definition", runner: &Runner{promptDefs: &inspectionPromptRepository{}}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := tc.runner.InspectPrompt(context.Background(), "prompt") if !errors.Is(err, ErrPromptLoad) { t.Fatalf("inspection error=%v, want prompt load", err) } }) } }) } func TestRunnerPrepareUsesThePromptInspectionSelectionAndHash(t *testing.T) { definition := promptDef(domain.FormatMarkdown, domain.ValidationBasic, 0) repository := &fakePromptRepo{def: definition} runner := NewRunner( repository, &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}}, &fakeArtifactReader{}, &fakeRenderer{rendered: &domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hello"}}}}, nil, nil, nil, ) inspection, err := runner.InspectPrompt(context.Background(), definition.ID) if err != nil { t.Fatalf("inspect prompt: %v", err) } prepared, err := runner.Prepare(context.Background(), domain.RunRequest{PromptID: definition.ID, ProfileID: "exec"}) if err != nil { t.Fatalf("prepare prompt: %v", err) } if inspection.PromptHash != prepared.PromptHash { t.Fatalf("inspection hash=%q, preparation hash=%q", inspection.PromptHash, prepared.PromptHash) } }