package processing import ( "context" "strings" "testing" ) 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{}, } in := ProductInput{ Name: "Widget", Description: "A widget", Mapped: map[string]any{"name": "Widget", "description": "A widget"}, PriorProcessedName: "Cached Widget", PriorProcessedDescription: "Cached description.", } // 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" normDesc := "A widget" 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) } if out.ProcessedDescription != "Cached description." { 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") } 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) } }