mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-01 21:37:33 +08:00
Ports doc-level auto-metadata extraction to Go and adds the knowledge_compiler component with scheduler/routing. Fixes Extractor metadata injection type assertion and enable_metadata default-on.
233 lines
7.4 KiB
Go
233 lines
7.4 KiB
Go
package wiki
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseOutline_Basic(t *testing.T) {
|
|
md := `# Main Title
|
|
|
|
Preamble paragraph that introduces the page.
|
|
|
|
## Background
|
|
|
|
This is the background section.
|
|
|
|
## Methods
|
|
|
|
This is the methods section.
|
|
|
|
### Subsection
|
|
|
|
Deeper detail.
|
|
|
|
## Conclusion
|
|
|
|
Final thoughts.`
|
|
sections := ParseOutline(md)
|
|
if len(sections) != 5 {
|
|
t.Fatalf("ParseOutline produced %d sections, want 5", len(sections))
|
|
}
|
|
titles := []string{"Main Title", "Background", "Methods", "Subsection", "Conclusion"}
|
|
levels := []int{1, 2, 2, 3, 2}
|
|
for i, sec := range sections {
|
|
if sec.Title != titles[i] {
|
|
t.Errorf("section %d title = %q, want %q", i, sec.Title, titles[i])
|
|
}
|
|
if sec.Level != levels[i] {
|
|
t.Errorf("section %d level = %d, want %d", i, sec.Level, levels[i])
|
|
}
|
|
if strings.TrimSpace(sec.Content) == "" {
|
|
t.Errorf("section %d content is empty", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseOutline_Empty(t *testing.T) {
|
|
if got := ParseOutline(""); len(got) != 0 {
|
|
t.Fatalf("ParseOutline(\"\") = %d sections, want 0", len(got))
|
|
}
|
|
if got := ParseOutline(" \n\n \n"); len(got) != 0 {
|
|
t.Fatalf("ParseOutline(whitespace) = %d sections, want 0", len(got))
|
|
}
|
|
}
|
|
|
|
func TestParseOutline_PreambleOnly(t *testing.T) {
|
|
// Markdown with no headings at all: the prose still becomes a synthetic
|
|
// level-0 section so the page body is not silently dropped.
|
|
sections := ParseOutline("Just some prose, no headings here.")
|
|
if len(sections) != 1 {
|
|
t.Fatalf("preamble-only produced %d sections, want 1", len(sections))
|
|
}
|
|
if sections[0].Level != 0 || sections[0].Title != "" {
|
|
t.Errorf("synthetic preamble = level=%d title=%q, want level=0 title=\"\"", sections[0].Level, sections[0].Title)
|
|
}
|
|
if !strings.Contains(sections[0].Content, "Just some prose") {
|
|
t.Errorf("synthetic preamble content missing prose: %q", sections[0].Content)
|
|
}
|
|
}
|
|
|
|
func TestParseOutline_StripsEmphasis(t *testing.T) {
|
|
md := "## **Bold heading**"
|
|
sections := ParseOutline(md)
|
|
if len(sections) != 1 || sections[0].Title != "Bold heading" {
|
|
t.Fatalf("emphasis strip: got %#v, want title \"Bold heading\"", sections)
|
|
}
|
|
}
|
|
|
|
func TestBuildPageProducts_PageAndSections(t *testing.T) {
|
|
md := `# The Page Title
|
|
|
|
Lead paragraph.
|
|
|
|
## Section A
|
|
|
|
A body.
|
|
|
|
## Section B
|
|
|
|
B body.
|
|
|
|
### Subsection B1
|
|
|
|
Deeper.`
|
|
products := buildPageProducts("t1", "d1", md, []string{"c1", "c2"})
|
|
if len(products) < 2 {
|
|
t.Fatalf("buildPageProducts = %d products, want page + 3 sections (>= 4)", len(products))
|
|
}
|
|
// First must be the page itself.
|
|
page := products[0]
|
|
if kind, _ := page.Meta["kind"].(string); kind != "page" {
|
|
t.Fatalf("products[0].kind = %q, want \"page\"", page.Meta["kind"])
|
|
}
|
|
if title, _ := page.Meta["title"].(string); title != "The Page Title" {
|
|
t.Errorf("page title = %q, want \"The Page Title\"", title)
|
|
}
|
|
if slug, _ := page.Meta["slug"].(string); slug != "the-page-title" {
|
|
t.Errorf("page slug = %q, want \"the-page-title\"", slug)
|
|
}
|
|
if ids, _ := page.Meta["source_chunk_ids"].([]string); len(ids) != 2 {
|
|
t.Errorf("page source_chunk_ids = %#v, want 2 ids", page.Meta["source_chunk_ids"])
|
|
}
|
|
// Sections are linked via parent_id = page.ID.
|
|
for i, p := range products[1:] {
|
|
if p.ParentID != page.ID {
|
|
t.Errorf("section[%d].parent_id = %q, want %q", i, p.ParentID, page.ID)
|
|
}
|
|
if kind, _ := p.Meta["kind"].(string); kind != "section" {
|
|
t.Errorf("section[%d].kind = %q, want \"section\"", i, p.Meta["kind"])
|
|
}
|
|
if _, has := p.Meta["section_level"]; !has {
|
|
t.Errorf("section[%d] missing section_level meta", i)
|
|
}
|
|
if ids, _ := p.Meta["source_chunk_ids"].([]string); len(ids) != 2 {
|
|
t.Errorf("section[%d] source_chunk_ids = %#v, want 2 ids", i, p.Meta["source_chunk_ids"])
|
|
}
|
|
if strings.TrimSpace(p.Content) == "" {
|
|
t.Errorf("section[%d] content is empty", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildPageProducts_NoSections(t *testing.T) {
|
|
// A page that contains only preamble: still emits a page product, with
|
|
// zero section children (the preamble is rolled into the page summary).
|
|
products := buildPageProducts("t1", "d1", "Just prose, no headings.", []string{"c1"})
|
|
if len(products) != 1 {
|
|
t.Fatalf("preamble-only page = %d products, want 1", len(products))
|
|
}
|
|
if kind, _ := products[0].Meta["kind"].(string); kind != "page" {
|
|
t.Errorf("kind = %q, want page", products[0].Meta["kind"])
|
|
}
|
|
}
|
|
|
|
func TestBuildPageProducts_StableIDs(t *testing.T) {
|
|
md := "# Title\n\n## A\n\nbody\n"
|
|
first := buildPageProducts("t1", "d1", md, []string{"c1"})
|
|
second := buildPageProducts("t1", "d1", md, []string{"c1"})
|
|
if len(first) != len(second) {
|
|
t.Fatalf("length mismatch: %d vs %d", len(first), len(second))
|
|
}
|
|
for i := range first {
|
|
if first[i].ID != second[i].ID {
|
|
t.Errorf("product[%d] id not stable: %q vs %q", i, first[i].ID, second[i].ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildWikiPageProducts_MultiPage(t *testing.T) {
|
|
pages := []wikiPageResult{
|
|
{
|
|
Slug: "entity/alpha",
|
|
Title: "Alpha",
|
|
PageType: "entity",
|
|
ContentRaw: "# Alpha\n\nSee [[concept/beta]].\n\n## Details\n\nAlpha body.",
|
|
Content: "# Alpha\n\nSee [Beta](artifact/kb/concept/beta).\n\n## Details\n\nAlpha body.",
|
|
SourceChunkIDs: []string{"c1"},
|
|
SourceDocIDs: []string{"d1"},
|
|
Outlinks: []string{"concept/beta"},
|
|
},
|
|
{
|
|
Slug: "concept/beta",
|
|
Title: "Beta",
|
|
PageType: "concept",
|
|
ContentRaw: "# Beta\n\nBody.",
|
|
Content: "# Beta\n\nBody.",
|
|
SourceChunkIDs: []string{"c2"},
|
|
SourceDocIDs: []string{"d1"},
|
|
},
|
|
}
|
|
products := buildWikiPageProducts("t1", "d1", pages)
|
|
if len(products) != 5 {
|
|
t.Fatalf("buildWikiPageProducts = %d products, want 5", len(products))
|
|
}
|
|
page := products[0]
|
|
if kind, _ := page.Meta["kind"].(string); kind != "page" {
|
|
t.Fatalf("page kind = %q, want page", page.Meta["kind"])
|
|
}
|
|
if outlinks, _ := page.Meta["outlinks"].([]string); len(outlinks) != 1 || outlinks[0] != "concept/beta" {
|
|
t.Fatalf("page outlinks = %#v, want concept/beta", page.Meta["outlinks"])
|
|
}
|
|
if ids, _ := page.Meta["source_chunk_ids"].([]string); len(ids) != 1 || ids[0] != "c1" {
|
|
t.Fatalf("page source_chunk_ids = %#v, want c1", page.Meta["source_chunk_ids"])
|
|
}
|
|
if ids, _ := page.Meta["source_doc_ids"].([]string); len(ids) != 1 || ids[0] != "d1" {
|
|
t.Fatalf("page source_doc_ids = %#v, want d1", page.Meta["source_doc_ids"])
|
|
}
|
|
}
|
|
|
|
func TestTransformWikiLinks(t *testing.T) {
|
|
rendered, outlinks := transformWikiLinks(
|
|
"See [[concept/beta|Beta]] and [[entity/alpha]]. Also [Beta](artifact/kb/concept/beta).",
|
|
"kb",
|
|
map[string]string{"entity/alpha": "Alpha", "concept/beta": "Beta"},
|
|
)
|
|
if !strings.Contains(rendered, "(artifact/kb/concept/beta)") || !strings.Contains(rendered, "(artifact/kb/entity/alpha)") {
|
|
t.Fatalf("rendered links not rewritten: %q", rendered)
|
|
}
|
|
if len(outlinks) != 2 {
|
|
t.Fatalf("outlinks = %#v, want 2 unique slugs", outlinks)
|
|
}
|
|
}
|
|
|
|
func TestSlugify(t *testing.T) {
|
|
cases := []struct{ in, want string }{
|
|
{"Hello World", "hello-world"},
|
|
{" Trim Me ", "trim-me"},
|
|
{"Punctuation!@#Stripped", "punctuationstripped"},
|
|
{"", "page"},
|
|
// Non-ASCII (CJK) letters are preserved (M17), so the slug keeps the
|
|
// Chinese characters and the trailing ASCII word.
|
|
{"中文 title", "中文-title"},
|
|
{"a-b-c", "a-b-c"},
|
|
// Punctuation-only title yields a hash fallback, not the bare "page".
|
|
{"!@#", "page-8b024aa5"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := slugify(c.in); got != c.want {
|
|
t.Errorf("slugify(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|