Files
Agent-System-Backend/internal/ingest/ooxml_python_test.go
weichen 727f3702d0
ci / vet-and-test (pull_request) Successful in 46s
Expose builtin document parsing through the host handle
Why: downstream parsers are unreachable via extension.Host and the base lacked xlsx/pptx parsers; MS1 needs Excel and PPT ingest (EXT-5 request, documents proposal 2026-10-06-ext-5-host-parse-document).

Evidence: local GOWORK=off go test ./... all 29 packages pass incl. new ooxml_test (hand-built parts), ooxml_python_test (openpyxl/python-pptx round-trip), inproc owner-scope and limit tests, xlsx upload e2e; go vet clean; recorded as SELF on the Dev task card, PR CI to confirm.

Impact: Host gains ParseDocument (pure compute; errors only on ctx cancel or >100MiB input); builtin:xlsx (sheet order via workbook.xml rels, rich-text shared strings, formula cache, 1900/1904 dates, sparse cells) and builtin:pptx (sldIdLst order, titles, group shapes, AlternateContent Choice, tables, notes) added with 200k-cell/64MiB-part caps; legacy doc/xls/ppt get an actionable hint; ParsersDescription gains xlsx/pptx and the missing text ext; surface snapshot now records exported interface methods; docx part reads share the 64MiB cap.

Refs: documents 2026-10-06-ext-5-host-parse-document.md; EXT-1 host.go precedent.
2026-10-06 14:59:38 +00:00

141 lines
4.1 KiB
Go

package ingest
// 真实生成器轮测:openpyxl / python-pptx 现场生成 xlsx/pptx 再解析,防止
// 手工夹具只验证"自己写的 XML"。生成器缺失时 t.Skip(诚实出口);CI 已装
// 这两个包(.gitea/workflows/ci.yaml 安装 python-docx python-pptx openpyxl)。
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
core "agent-system-core"
)
func pythonModuleAvailable(t *testing.T, module string) bool {
t.Helper()
if _, err := exec.LookPath("python3"); err != nil {
return false
}
return exec.Command("python3", "-c", "import "+module).Run() == nil
}
// runPythonScript 把脚本写到临时目录执行;失败即 t.Fatal(脚本属测试自身)。
func runPythonScript(t *testing.T, script string) {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "gen.py")
if err := os.WriteFile(path, []byte(script), 0o644); err != nil {
t.Fatal(err)
}
out, err := exec.Command("python3", path).CombinedOutput()
if err != nil {
t.Fatalf("python generator failed: %v\n%s", err, out)
}
}
func TestParseXlsxFromOpenpyxl(t *testing.T) {
if !pythonModuleAvailable(t, "openpyxl") {
t.Skip("python3 openpyxl not available")
}
dir := t.TempDir()
file := filepath.Join(dir, "报价单.xlsx")
runPythonScript(t, `
import openpyxl
from datetime import datetime
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "报价"
ws.append(["物料", "单价", "日期"])
ws.append(["支架", 12.5, datetime(2024, 1, 1)])
ws["C2"].number_format = "yyyy-mm-dd"
ws["D2"] = "=A2&\"-x\""
ws["D3"] = True
wb.create_sheet("说明").append(["备注行"])
wb.save(r"`+file+`")
`)
data, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
doc := Parse(file, "", data)
if doc.Meta.Parser != "builtin:xlsx" || len(doc.Warnings) != 0 {
t.Fatalf("meta: %+v warnings: %v", doc.Meta, doc.Warnings)
}
var heads []string
for _, b := range doc.Blocks {
if b.Type == core.BlockHeading {
heads = append(heads, b.Text)
}
}
if len(heads) != 2 || heads[0] != "报价" || heads[1] != "说明" {
t.Fatalf("headings: %v", heads)
}
plain := doc.PlainText()
for _, want := range []string{"物料", "支架", "12.5", "2024-01-01"} {
if !strings.Contains(plain, want) {
t.Errorf("missing %q in: %s", want, plain)
}
}
// openpyxl 公式不带缓存值:回显公式本身(可检索、不冒充结果)。
if !strings.Contains(plain, "=A2") {
t.Errorf("formula without cache should echo: %s", plain)
}
if !strings.Contains(plain, "TRUE") {
t.Errorf("boolean cell should render TRUE: %s", plain)
}
}
func TestParsePptxFromPythonPptx(t *testing.T) {
if !pythonModuleAvailable(t, "pptx") {
t.Skip("python3 pptx not available")
}
dir := t.TempDir()
file := filepath.Join(dir, "方案评审.pptx")
runPythonScript(t, `
from pptx import Presentation
from pptx.util import Inches
prs = Presentation()
s1 = prs.slides.add_slide(prs.slide_layouts[0])
s1.shapes.title.text = "评审结论"
s1.placeholders[1].text = "通过"
s2 = prs.slides.add_slide(prs.slide_layouts[5])
s2.shapes.title.text = "报价明细"
tbl = s2.shapes.add_table(2, 2, Inches(1), Inches(2), Inches(6), Inches(1)).table
tbl.cell(0, 0).text = "物料"
tbl.cell(0, 1).text = "单价"
tbl.cell(1, 0).text = "支架"
tbl.cell(1, 1).text = "12.5"
notes = s1.notes_slide.notes_text_frame
notes.text = "需要补充法规依据"
prs.save(r"`+file+`")
`)
data, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
doc := Parse(file, "", data)
if doc.Meta.Parser != "builtin:pptx" || len(doc.Warnings) != 0 {
t.Fatalf("meta: %+v warnings: %v", doc.Meta, doc.Warnings)
}
var heads []string
for _, b := range doc.Blocks {
if b.Type == core.BlockHeading {
heads = append(heads, b.Text)
}
}
if len(heads) != 2 || !strings.HasPrefix(heads[0], "幻灯片 1:评审结论") || !strings.HasPrefix(heads[1], "幻灯片 2:报价明细") {
t.Fatalf("headings: %v", heads)
}
plain := doc.PlainText()
for _, want := range []string{"通过", "物料", "支架", "12.5", "备注:需要补充法规依据"} {
if !strings.Contains(plain, want) {
t.Errorf("missing %q in: %s", want, plain)
}
}
}