mirror of
https://github.com/karust/openserp.git
synced 2026-08-05 16:53:54 +08:00
266 lines
8.7 KiB
Go
266 lines
8.7 KiB
Go
package core
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
)
|
|
|
|
func TestEnvelopeAlwaysIncludesSerpFeatures(t *testing.T) {
|
|
env := NewEnvelope(Query{Text: "golang"}, "req-1", time.Unix(0, 0), []string{"google"})
|
|
|
|
data, err := json.Marshal(env)
|
|
if err != nil {
|
|
t.Fatalf("marshal envelope: %v", err)
|
|
}
|
|
if !strings.Contains(string(data), `"serp_features":[]`) {
|
|
t.Fatalf("expected empty serp_features array in JSON, got %s", data)
|
|
}
|
|
}
|
|
|
|
func TestAppendEnrichedSearchResultAddsFeatureSurface(t *testing.T) {
|
|
env := NewEnvelope(Query{Text: "weather"}, "req-1", time.Unix(0, 0), []string{"google"})
|
|
raw := SearchResult{
|
|
Rank: 1,
|
|
URL: "https://example.com/weather",
|
|
Title: "Weather result",
|
|
Description: "Organic snippet",
|
|
Features: []SerpFeature{{
|
|
Type: ResultTypeAnswerBox,
|
|
Text: "72 F and sunny",
|
|
Links: []FeatureLink{{
|
|
Title: "Weather source",
|
|
URL: "https://example.com/weather",
|
|
}},
|
|
Position: &Position{Absolute: 1},
|
|
Confidence: 0.9,
|
|
}},
|
|
}
|
|
|
|
AppendEnrichedSearchResult(env, raw, EnrichContext{Engine: "google", Query: Query{}}, time.Unix(0, 0))
|
|
|
|
if len(env.Results) != 1 {
|
|
t.Fatalf("expected 1 result, got %d", len(env.Results))
|
|
}
|
|
if len(env.SerpFeatures) != 1 {
|
|
t.Fatalf("expected 1 feature, got %d", len(env.SerpFeatures))
|
|
}
|
|
feature := env.SerpFeatures[0]
|
|
if feature.ID == "" || !strings.HasPrefix(feature.ID, "f_") {
|
|
t.Fatalf("expected stable feature ID, got %q", feature.ID)
|
|
}
|
|
if feature.Engine != "google" {
|
|
t.Fatalf("feature engine = %q, want google", feature.Engine)
|
|
}
|
|
if feature.ExtractedAt != "1970-01-01T00:00:00Z" {
|
|
t.Fatalf("feature extracted_at = %q", feature.ExtractedAt)
|
|
}
|
|
if len(feature.SourceResultIDs) != 1 || feature.SourceResultIDs[0] != env.Results[0].ID {
|
|
t.Fatalf("expected feature to reference result ID %q, got %#v", env.Results[0].ID, feature.SourceResultIDs)
|
|
}
|
|
}
|
|
|
|
func TestAppendEnrichedSearchResultMirrorsExistingAnswerResultAsFeature(t *testing.T) {
|
|
env := NewEnvelope(Query{Text: "weather"}, "req-1", time.Unix(0, 0), []string{"google"})
|
|
raw := SearchResult{
|
|
Rank: -1,
|
|
Type: ResultTypeAnswerBox,
|
|
URL: "https://example.com/weather",
|
|
Title: "Weather",
|
|
Description: "72 F and sunny",
|
|
}
|
|
|
|
AppendEnrichedSearchResult(env, raw, EnrichContext{Engine: "google", Query: Query{}}, time.Unix(0, 0))
|
|
|
|
if len(env.Results) != 1 {
|
|
t.Fatalf("expected existing result to be preserved, got %d results", len(env.Results))
|
|
}
|
|
if len(env.SerpFeatures) != 1 {
|
|
t.Fatalf("expected mirrored feature, got %d", len(env.SerpFeatures))
|
|
}
|
|
if env.SerpFeatures[0].Type != ResultTypeAnswerBox {
|
|
t.Fatalf("mirrored feature type = %q", env.SerpFeatures[0].Type)
|
|
}
|
|
if env.SerpFeatures[0].Text != "72 F and sunny" {
|
|
t.Fatalf("mirrored feature text = %q", env.SerpFeatures[0].Text)
|
|
}
|
|
if len(env.SerpFeatures[0].SourceResultIDs) != 1 || env.SerpFeatures[0].SourceResultIDs[0] != env.Results[0].ID {
|
|
t.Fatalf("mirrored feature did not reference result: %#v", env.SerpFeatures[0].SourceResultIDs)
|
|
}
|
|
}
|
|
|
|
func TestStripResultFeatures(t *testing.T) {
|
|
results := []SearchResult{
|
|
{
|
|
Rank: 1,
|
|
URL: "https://example.com",
|
|
Title: "Example",
|
|
Features: []SerpFeature{{
|
|
Type: ResultTypeRelatedSearches,
|
|
Items: []FeatureItem{
|
|
{Text: "example search"},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
Rank: 2,
|
|
URL: "https://example.org",
|
|
Title: "Example Org",
|
|
},
|
|
}
|
|
|
|
if kept := StripResultFeatures(results, true); len(kept[0].Features) != 1 {
|
|
t.Fatalf("keep=true must preserve features, got %#v", kept[0].Features)
|
|
}
|
|
|
|
stripped := StripResultFeatures(results, false)
|
|
|
|
if len(stripped) != 2 {
|
|
t.Fatalf("expected result count to be preserved, got %d", len(stripped))
|
|
}
|
|
for i, result := range stripped {
|
|
if len(result.Features) != 0 {
|
|
t.Fatalf("result %d kept features: %#v", i, result.Features)
|
|
}
|
|
}
|
|
if stripped[0].URL != "https://example.com" || stripped[1].Rank != 2 {
|
|
t.Fatalf("non-feature fields changed: %#v", stripped)
|
|
}
|
|
}
|
|
|
|
func TestFeatureItemsUseTextAsTitleAndStripInvisibleCharacters(t *testing.T) {
|
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(`
|
|
<div class="related">
|
|
<a href="?q=python+coding">python coding​</a>
|
|
</div>`))
|
|
if err != nil {
|
|
t.Fatalf("parse fixture: %v", err)
|
|
}
|
|
|
|
features := ExtractSerpFeaturesBySelectors(doc, []SerpFeatureSelector{{
|
|
Type: ResultTypeRelatedSearches,
|
|
Title: "Related searches",
|
|
Container: []string{".related"},
|
|
ItemSelector: []string{"a"},
|
|
LinkSelector: []string{"a"},
|
|
}})
|
|
|
|
if len(features) != 1 || len(features[0].Items) != 1 {
|
|
t.Fatalf("expected one feature item, got %#v", features)
|
|
}
|
|
item := features[0].Items[0]
|
|
if item.Title != "python coding" || item.Text != "python coding" {
|
|
t.Fatalf("unexpected item text/title: %#v", item)
|
|
}
|
|
}
|
|
|
|
func TestEnrichSerpFeatureNormalizesRelativeLinks(t *testing.T) {
|
|
feature := EnrichSerpFeature(SerpFeature{
|
|
Type: ResultTypeRelatedSearches,
|
|
Items: []FeatureItem{{
|
|
Text: "python coding\u200b",
|
|
Link: "?q=python+coding&t=h",
|
|
}},
|
|
Links: []FeatureLink{{
|
|
Title: "baidu related",
|
|
URL: "/s?wd=python",
|
|
}},
|
|
}, "duckduckgo", "", time.Unix(0, 0))
|
|
|
|
if feature.Items[0].Title != "python coding" || feature.Items[0].Text != "python coding" {
|
|
t.Fatalf("unexpected enriched item: %#v", feature.Items[0])
|
|
}
|
|
if got, want := feature.Items[0].Link, "https://duckduckgo.com/?q=python+coding&t=h"; got != want {
|
|
t.Fatalf("item link = %q, want %q", got, want)
|
|
}
|
|
if got, want := feature.Links[0].URL, "https://duckduckgo.com/s?wd=python"; got != want {
|
|
t.Fatalf("feature link = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRenderersIncludeSerpFeatures(t *testing.T) {
|
|
env := NewEnvelope(Query{Text: "openserp"}, "req-1", time.Unix(0, 0), []string{"google"})
|
|
AppendEnrichedSearchResult(env, SearchResult{
|
|
Rank: 1,
|
|
URL: "https://example.com/result",
|
|
Title: "Organic result",
|
|
Description: "Snippet",
|
|
Features: []SerpFeature{
|
|
{
|
|
Type: ResultTypeAISummary,
|
|
Text: "OpenSERP is a search API.",
|
|
Links: []FeatureLink{{
|
|
Title: "Example",
|
|
URL: "https://example.com/source",
|
|
}},
|
|
Position: &Position{Absolute: 1},
|
|
Confidence: 0.95,
|
|
},
|
|
{
|
|
Type: ResultTypeRelatedSearches,
|
|
Items: []FeatureItem{
|
|
{Text: "openserp cloud"},
|
|
{Text: "serp api"},
|
|
},
|
|
},
|
|
},
|
|
}, EnrichContext{Engine: "google", Query: Query{}}, time.Unix(0, 0))
|
|
env.Finalize(time.Unix(0, 0), Query{Text: "openserp", Limit: 10})
|
|
|
|
markdown := string(RenderMarkdown(env))
|
|
if !strings.Contains(markdown, "## AI summary") || !strings.Contains(markdown, "Sources:") {
|
|
t.Fatalf("markdown missing AI summary feature section:\n%s", markdown)
|
|
}
|
|
if !strings.Contains(markdown, "## Results") {
|
|
t.Fatalf("markdown missing Results section:\n%s", markdown)
|
|
}
|
|
if !strings.Contains(markdown, "## Related searches") {
|
|
t.Fatalf("markdown missing related searches section:\n%s", markdown)
|
|
}
|
|
// Spec order: AI summary -> ... -> Results -> Related searches.
|
|
if aiIdx, resIdx, relIdx := strings.Index(markdown, "## AI summary"), strings.Index(markdown, "## Results"), strings.Index(markdown, "## Related searches"); !(aiIdx < resIdx && resIdx < relIdx) {
|
|
t.Fatalf("markdown section order wrong: ai=%d results=%d related=%d\n%s", aiIdx, resIdx, relIdx, markdown)
|
|
}
|
|
|
|
text := string(RenderText(env))
|
|
if !strings.Contains(text, "AI summary\n") || !strings.Contains(text, "Related searches\n") {
|
|
t.Fatalf("text missing feature sections:\n%s", text)
|
|
}
|
|
|
|
lines := strings.Split(strings.TrimSpace(string(RenderNDJSON(env))), "\n")
|
|
if len(lines) != 3 {
|
|
t.Fatalf("expected 3 ndjson lines, got %d: %v", len(lines), lines)
|
|
}
|
|
if !strings.Contains(lines[0], `"kind":"result"`) {
|
|
t.Fatalf("first ndjson line should be a result, got %s", lines[0])
|
|
}
|
|
if !strings.Contains(lines[1], `"kind":"feature"`) || !strings.Contains(lines[2], `"kind":"feature"`) {
|
|
t.Fatalf("feature ndjson lines missing kind tag: %v", lines)
|
|
}
|
|
}
|
|
|
|
// TestRenderFeaturesUnorderedTypeStillRenders guards against silently dropping a
|
|
// feature whose Type is in neither render-order section (e.g. a newly added
|
|
// enum value not yet placed). It must still appear in text and markdown output.
|
|
func TestRenderFeaturesUnorderedTypeStillRenders(t *testing.T) {
|
|
const unplaced ResultType = "experimental_module"
|
|
env := NewEnvelope(Query{Text: "openserp"}, "req-1", time.Unix(0, 0), []string{"google"})
|
|
env.SerpFeatures = append(env.SerpFeatures, SerpFeature{
|
|
Type: unplaced,
|
|
Text: "experimental feature body",
|
|
})
|
|
|
|
text := string(RenderText(env))
|
|
if !strings.Contains(text, "experimental feature body") {
|
|
t.Fatalf("text output dropped an unordered feature type:\n%s", text)
|
|
}
|
|
|
|
markdown := string(RenderMarkdown(env))
|
|
if !strings.Contains(markdown, "experimental feature body") {
|
|
t.Fatalf("markdown output dropped an unordered feature type:\n%s", markdown)
|
|
}
|
|
}
|