2026-08-09 22:47:43 +02:00
|
|
|
package processing
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
2026-08-16 16:57:36 +02:00
|
|
|
|
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
2026-08-09 22:47:43 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestHashEnhanceInput_stableAndSensitive(t *testing.T) {
|
|
|
|
|
attrs := map[string]any{"brand": "Acme", "color": "Red"}
|
|
|
|
|
a := HashEnhanceInput("Shoes", "Runner", "A shoe", "", "", "", "", attrs)
|
|
|
|
|
b := HashEnhanceInput("Shoes", "Runner", "A shoe", "", "", "", "", attrs)
|
|
|
|
|
if a == "" || a != b {
|
|
|
|
|
t.Fatalf("expected stable hash, got %q vs %q", a, b)
|
|
|
|
|
}
|
|
|
|
|
if HashEnhanceInput("Shoes", "Runner X", "A shoe", "", "", "", "", attrs) == a {
|
|
|
|
|
t.Fatal("name change must change hash")
|
|
|
|
|
}
|
|
|
|
|
if HashEnhanceInput("Shoes", "Runner", "A shoe", "Brand:\n- tone: bold", "", "", "", attrs) == a {
|
|
|
|
|
t.Fatal("brand prompt must change hash")
|
|
|
|
|
}
|
|
|
|
|
if HashEnhanceInput("Shoes", "Runner", "A shoe", "", "", "sys-a", "user-a", attrs) == a {
|
|
|
|
|
t.Fatal("prompt template change must change hash")
|
|
|
|
|
}
|
|
|
|
|
if HashEnhanceInput("Shoes", "Runner", "A shoe", "", "fr", "", "", attrs) == a {
|
|
|
|
|
t.Fatal("language change must change hash")
|
|
|
|
|
}
|
|
|
|
|
// Attr key order must not matter (CompactAttrs + json map sort).
|
|
|
|
|
attrs2 := map[string]any{"color": "Red", "brand": "Acme"}
|
|
|
|
|
if HashEnhanceInput("Shoes", "Runner", "A shoe", "", "", "", "", attrs2) != a {
|
|
|
|
|
t.Fatal("attr key order must not change hash")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRunSteps_skipsEnhanceWhenInputHashUnchanged(t *testing.T) {
|
|
|
|
|
calls := 0
|
|
|
|
|
e := &Engine{
|
|
|
|
|
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
|
|
|
|
calls++
|
|
|
|
|
return Completion{Text: `{"name":"ShouldNotRun","description":"Nope"}`, TotalTokens: 9}, nil
|
|
|
|
|
}},
|
|
|
|
|
Vector: NoopVectorCategorizer{},
|
|
|
|
|
}
|
2026-08-16 16:57:36 +02:00
|
|
|
priorDesc := "Cached description with enough retail detail for listing copy."
|
2026-08-09 22:47:43 +02:00
|
|
|
in := ProductInput{
|
|
|
|
|
Name: "Widget",
|
2026-08-16 16:57:36 +02:00
|
|
|
Description: "A widget with enough mapped detail for hashing.",
|
|
|
|
|
Mapped: map[string]any{"name": "Widget", "description": "A widget with enough mapped detail for hashing."},
|
2026-08-09 22:47:43 +02:00
|
|
|
PriorProcessedName: "Cached Widget",
|
2026-08-16 16:57:36 +02:00
|
|
|
PriorProcessedDescription: priorDesc,
|
2026-08-09 22:47:43 +02:00
|
|
|
}
|
|
|
|
|
// First pass without prior hash to compute the hash shape via enhance path is awkward;
|
|
|
|
|
// compute the same hash RunSteps will see after normalize (name/desc from mapped).
|
|
|
|
|
// enhance_only: normalize then enhance with out.Name from normalized.
|
|
|
|
|
normName := "Widget"
|
2026-08-16 16:57:36 +02:00
|
|
|
normDesc := "A widget with enough mapped detail for hashing."
|
2026-08-09 22:47:43 +02:00
|
|
|
sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{})
|
|
|
|
|
in.PriorEnhanceHash = HashEnhanceInput("", normName, normDesc, "", "", sysTpl, userTpl, map[string]any{})
|
|
|
|
|
|
|
|
|
|
out, err := e.RunSteps(context.Background(), "co", in, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if calls != 0 {
|
|
|
|
|
t.Fatalf("expected LLM skip, calls=%d", calls)
|
|
|
|
|
}
|
|
|
|
|
if out.ProcessedName != "Cached Widget" {
|
|
|
|
|
t.Fatalf("name=%q", out.ProcessedName)
|
|
|
|
|
}
|
2026-08-16 16:57:36 +02:00
|
|
|
if out.ProcessedDescription != priorDesc {
|
2026-08-09 22:47:43 +02:00
|
|
|
t.Fatalf("desc=%q", out.ProcessedDescription)
|
|
|
|
|
}
|
|
|
|
|
if out.TotalTokens != 0 {
|
|
|
|
|
t.Fatalf("tokens=%d want 0", out.TotalTokens)
|
|
|
|
|
}
|
|
|
|
|
if out.FieldSources[FieldEnhanceInputHash] != in.PriorEnhanceHash {
|
|
|
|
|
t.Fatalf("hash field=%v", out.FieldSources[FieldEnhanceInputHash])
|
|
|
|
|
}
|
|
|
|
|
if src, _ := out.FieldSources["name"].(string); src != "ai_enhance_unchanged" {
|
|
|
|
|
t.Fatalf("name source=%v", out.FieldSources["name"])
|
|
|
|
|
}
|
|
|
|
|
if !out.SkipCreditDebit {
|
|
|
|
|
t.Fatal("expected SkipCreditDebit when enhance hash unchanged")
|
|
|
|
|
}
|
|
|
|
|
if shouldDebitProductProcessing(false, out) {
|
|
|
|
|
t.Fatal("processOne must not debit when enhance unchanged")
|
|
|
|
|
}
|
2026-08-16 16:57:36 +02:00
|
|
|
// Hash-skip (0 tokens) must still persist known engine mode, not "unknown".
|
|
|
|
|
if out.AIProviderMode != AIProviderInternal {
|
|
|
|
|
t.Fatalf("ai_provider_mode=%q want %q on hash-skip", out.AIProviderMode, AIProviderInternal)
|
|
|
|
|
}
|
|
|
|
|
// processOne treats unknown as empty and prefers job modeLabel.
|
|
|
|
|
got := preferKnownProviderMode(AIProviderUnknown, AIProviderInternal)
|
|
|
|
|
if got != AIProviderInternal {
|
|
|
|
|
t.Fatalf("preferKnownProviderMode(unknown, internal)=%q", got)
|
|
|
|
|
}
|
2026-08-09 22:47:43 +02:00
|
|
|
joined := strings.Join(out.Notes, ";")
|
|
|
|
|
if !strings.Contains(joined, "unchanged") {
|
|
|
|
|
t.Fatalf("notes=%v", out.Notes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRunSteps_callsEnhanceWhenInputHashDiffers(t *testing.T) {
|
|
|
|
|
calls := 0
|
|
|
|
|
e := &Engine{
|
|
|
|
|
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
|
|
|
|
calls++
|
|
|
|
|
return Completion{Text: `{"name":"Fresh","description":"New copy."}`, TotalTokens: 3}, nil
|
|
|
|
|
}},
|
|
|
|
|
Vector: NoopVectorCategorizer{},
|
|
|
|
|
}
|
|
|
|
|
out, err := e.RunSteps(context.Background(), "co", ProductInput{
|
|
|
|
|
Mapped: map[string]any{"name": "Widget", "description": "A widget"},
|
|
|
|
|
PriorEnhanceHash: "deadbeef",
|
|
|
|
|
PriorProcessedName: "Old",
|
|
|
|
|
PriorProcessedDescription: "Old desc",
|
|
|
|
|
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if calls != 1 {
|
|
|
|
|
t.Fatalf("calls=%d", calls)
|
|
|
|
|
}
|
|
|
|
|
if out.ProcessedName != "Fresh" {
|
|
|
|
|
t.Fatalf("name=%q", out.ProcessedName)
|
|
|
|
|
}
|
|
|
|
|
if out.TotalTokens != 3 {
|
|
|
|
|
t.Fatalf("tokens=%d", out.TotalTokens)
|
|
|
|
|
}
|
|
|
|
|
if out.SkipCreditDebit {
|
|
|
|
|
t.Fatal("hash miss must still debit")
|
|
|
|
|
}
|
|
|
|
|
if !shouldDebitProductProcessing(false, out) {
|
|
|
|
|
t.Fatal("expected debit when enhance ran")
|
|
|
|
|
}
|
|
|
|
|
h, _ := out.FieldSources[FieldEnhanceInputHash].(string)
|
|
|
|
|
if h == "" || h == "deadbeef" {
|
|
|
|
|
t.Fatalf("expected new hash in field_sources, got %q", h)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-16 16:57:36 +02:00
|
|
|
|
|
|
|
|
func TestRunSteps_reenhancesWhenPriorDescEqualsTitle(t *testing.T) {
|
|
|
|
|
calls := 0
|
|
|
|
|
e := &Engine{
|
|
|
|
|
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
|
|
|
|
calls++
|
|
|
|
|
return Completion{Text: `{"name":"Widget Pro","description":"A durable retail widget for everyday use with clear specs."}`, TotalTokens: 5}, nil
|
|
|
|
|
}},
|
|
|
|
|
Vector: NoopVectorCategorizer{},
|
|
|
|
|
}
|
|
|
|
|
in := ProductInput{
|
|
|
|
|
Name: "Widget",
|
|
|
|
|
Description: "Widget",
|
|
|
|
|
Mapped: map[string]any{"name": "Widget", "description": "Widget"},
|
|
|
|
|
PriorProcessedName: "Widget",
|
|
|
|
|
PriorProcessedDescription: "Widget",
|
|
|
|
|
}
|
|
|
|
|
sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{})
|
|
|
|
|
in.PriorEnhanceHash = HashEnhanceInput("", "Widget", "Widget", "", "", sysTpl, userTpl, map[string]any{})
|
|
|
|
|
|
|
|
|
|
out, err := e.RunSteps(context.Background(), "co", in, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if calls != 1 {
|
|
|
|
|
t.Fatalf("expected LLM re-enhance when prior desc equals title, calls=%d", calls)
|
|
|
|
|
}
|
|
|
|
|
if out.ProcessedDescription == "Widget" || out.ProcessedDescription == "" {
|
|
|
|
|
t.Fatalf("desc=%q", out.ProcessedDescription)
|
|
|
|
|
}
|
|
|
|
|
if out.SkipCreditDebit {
|
|
|
|
|
t.Fatal("title-echo prior must still debit")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPreferredProductDescription_skipsTitleEcho(t *testing.T) {
|
|
|
|
|
title := "Acme Widget Pro"
|
|
|
|
|
if got := preferredProductDescription(title, title, "Acme Widget Pro.", "A durable retail widget for everyday use."); got != "A durable retail widget for everyday use." {
|
|
|
|
|
t.Fatalf("got %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := preferredProductDescription(title, title, strings.ToUpper(title)); got != "" {
|
|
|
|
|
t.Fatalf("expected empty when all candidates echo title, got %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := preferredProductDescription(title, "", "<nil>", "Category:"); got != "" {
|
|
|
|
|
t.Fatalf("expected empty for unusable candidates, got %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRunSteps_emptyDescriptionGetsNonemptyProcessedDesc(t *testing.T) {
|
|
|
|
|
calls := 0
|
|
|
|
|
e := &Engine{
|
|
|
|
|
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
|
|
|
|
calls++
|
|
|
|
|
return Completion{Text: `{"name":"Gigabyte Monitor","description":""}`, TotalTokens: 4}, nil
|
|
|
|
|
}},
|
|
|
|
|
Vector: NoopVectorCategorizer{},
|
|
|
|
|
}
|
|
|
|
|
out, err := e.RunSteps(context.Background(), "co", ProductInput{
|
|
|
|
|
Name: "Gigabyte Monitor",
|
|
|
|
|
Mapped: map[string]any{"name": "Gigabyte Monitor", "brand": "Gigabyte"},
|
|
|
|
|
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if calls != 1 {
|
|
|
|
|
t.Fatalf("expected enhance call, calls=%d", calls)
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(out.ProcessedDescription) == "" {
|
|
|
|
|
t.Fatal("expected nonempty ProcessedDescription when title set")
|
|
|
|
|
}
|
|
|
|
|
if descriptionEchoesTitle(out.ProcessedDescription, out.ProcessedName) {
|
|
|
|
|
t.Fatalf("ProcessedDescription must not echo title: name=%q desc=%q", out.ProcessedName, out.ProcessedDescription)
|
|
|
|
|
}
|
|
|
|
|
if descriptionEchoesTitle(out.ProcessedDescription, "Gigabyte Monitor") {
|
|
|
|
|
t.Fatalf("ProcessedDescription must not echo mapped title: desc=%q", out.ProcessedDescription)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRunSteps_thinOKDoesNotPersistEnhanceHash(t *testing.T) {
|
|
|
|
|
e := &Engine{
|
|
|
|
|
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
|
|
|
|
return Completion{Text: `{"name":"Monitor","description":"ok"}`, TotalTokens: 2}, nil
|
|
|
|
|
}},
|
|
|
|
|
Vector: NoopVectorCategorizer{},
|
|
|
|
|
}
|
|
|
|
|
out, err := e.RunSteps(context.Background(), "co", ProductInput{
|
|
|
|
|
Name: "Monitor",
|
|
|
|
|
Description: "Monitor",
|
|
|
|
|
Mapped: map[string]any{"name": "Monitor", "description": "Monitor"},
|
|
|
|
|
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if h, _ := out.FieldSources[FieldEnhanceInputHash].(string); h != "" && isWeakPriorEnhanceDescription(out.ProcessedDescription, out.ProcessedName) {
|
|
|
|
|
t.Fatalf("must not persist hash with weak desc: hash=%q desc=%q", h, out.ProcessedDescription)
|
|
|
|
|
}
|
|
|
|
|
for _, lf := range out.LocalizedContent {
|
|
|
|
|
if lf.EnhanceInputHash != "" && isWeakPriorEnhanceDescription(lf.ProcessedDescription, lf.ProcessedName) {
|
|
|
|
|
t.Fatalf("localized hash poisoned with weak desc: %+v", lf)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRunSteps_weakPriorHashDoesNotSkipReprocess(t *testing.T) {
|
|
|
|
|
calls := 0
|
|
|
|
|
e := &Engine{
|
|
|
|
|
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
|
|
|
|
calls++
|
|
|
|
|
return Completion{Text: `{"name":"Widget Pro","description":"A durable retail widget for everyday use with clear specs."}`, TotalTokens: 5}, nil
|
|
|
|
|
}},
|
|
|
|
|
Vector: NoopVectorCategorizer{},
|
|
|
|
|
}
|
|
|
|
|
normName := "Widget"
|
|
|
|
|
normDesc := "Widget"
|
|
|
|
|
sysTpl, userTpl := resolveProductPromptTemplates(ProductInput{})
|
|
|
|
|
priorHash := HashEnhanceInput("", normName, normDesc, "", "", sysTpl, userTpl, map[string]any{})
|
|
|
|
|
out, err := e.RunSteps(context.Background(), "co", ProductInput{
|
|
|
|
|
Name: "Widget",
|
|
|
|
|
Description: "Widget",
|
|
|
|
|
Mapped: map[string]any{"name": "Widget", "description": "Widget"},
|
|
|
|
|
PriorEnhanceHash: priorHash,
|
|
|
|
|
PriorProcessedName: "Widget",
|
|
|
|
|
PriorProcessedDescription: "ok",
|
|
|
|
|
PriorLocalized: company.LocalizedContent{
|
|
|
|
|
"en": {ProcessedName: "Widget", ProcessedDescription: "ok", EnhanceInputHash: priorHash},
|
|
|
|
|
},
|
|
|
|
|
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if calls != 1 {
|
|
|
|
|
t.Fatalf("weak prior desc must force completer, calls=%d", calls)
|
|
|
|
|
}
|
|
|
|
|
if out.SkipCreditDebit {
|
|
|
|
|
t.Fatal("weak prior must not SkipCreditDebit")
|
|
|
|
|
}
|
|
|
|
|
if out.ProcessedDescription == "ok" || out.ProcessedDescription == "Widget" {
|
|
|
|
|
t.Fatalf("desc=%q", out.ProcessedDescription)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRunSteps_failedEnhanceDoesNotPoisonLocalizedHash(t *testing.T) {
|
|
|
|
|
e := &Engine{
|
|
|
|
|
Completer: stubCompleter{fn: func(system, user string) (Completion, error) {
|
|
|
|
|
return Completion{}, context.DeadlineExceeded
|
|
|
|
|
}},
|
|
|
|
|
Vector: NoopVectorCategorizer{},
|
|
|
|
|
}
|
|
|
|
|
out, err := e.RunSteps(context.Background(), "co", ProductInput{
|
|
|
|
|
Name: "Widget",
|
|
|
|
|
Mapped: map[string]any{"name": "Widget", "description": "A widget with enough mapped detail for hashing."},
|
|
|
|
|
PriorEnhanceHash: "should-not-survive",
|
|
|
|
|
PriorLocalized: company.LocalizedContent{
|
|
|
|
|
"en": {ProcessedName: "Old", ProcessedDescription: "Old desc that is long enough to look real.", EnhanceInputHash: "poison"},
|
|
|
|
|
},
|
|
|
|
|
}, "enhance_only", nil, StepPolicy{AllowAI: true, AllowEPREL: true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, ok := out.FieldSources[FieldEnhanceInputHash]; ok {
|
|
|
|
|
t.Fatalf("failed enhance must not keep field_sources hash, got %v", out.FieldSources[FieldEnhanceInputHash])
|
|
|
|
|
}
|
|
|
|
|
for lang, lf := range out.LocalizedContent {
|
|
|
|
|
if lf.EnhanceInputHash != "" {
|
|
|
|
|
t.Fatalf("lang %s: failed enhance must clear localized hash, got %q", lang, lf.EnhanceInputHash)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|