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 version string } func (r *inspectionPromptRepository) GetPromptDefinition( _ context.Context, id string, version string, ) (*domain.PromptDefinition, error) { r.calls++ r.id = id r.version = version if r.err != nil { return nil, r.err } return r.definition, nil } 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 ", " version ") 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 " || repository.version != " version " { t.Fatalf("prompt lookup=(calls=%d id=%q version=%q), want one unchanged lookup", repository.calls, repository.id, repository.version) } 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 ", " version ") 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 TestRunnerInspectPromptClassifiesFailuresWithoutRepositoryWorkAfterCancellation(t *testing.T) { t.Run("blank ID", func(t *testing.T) { repository := &inspectionPromptRepository{} _, err := (&Runner{promptDefs: repository}).InspectPrompt(context.Background(), " \t ", "version") 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", "version") 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", "version") 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", "version") 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()}}, nil, &fakeArtifactReader{}, &fakeRenderer{rendered: &domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hello"}}}}, nil, nil, nil, ) inspection, err := runner.InspectPrompt(context.Background(), definition.ID, definition.Version) if err != nil { t.Fatalf("inspect prompt: %v", err) } prepared, err := runner.Prepare(context.Background(), domain.RunRequest{PromptID: definition.ID, PromptVersion: definition.Version, 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) } }