WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
package drive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/datasource/connector/feishu/core"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
)
|
||||
|
||||
// makeDriveConfig builds a DataSourceConfig for the Drive connector, mirroring
|
||||
// makeConfig but with the drive connector type and a multimodal toggle.
|
||||
func makeDriveConfig(cfg *core.Config, resourceIDs []string, multimodal bool) *types.DataSourceConfig {
|
||||
c := makeConfig(cfg, resourceIDs)
|
||||
c.Type = types.ConnectorTypeFeishuDrive
|
||||
c.MultimodalEnabled = multimodal
|
||||
return c
|
||||
}
|
||||
|
||||
// fakeFeishuDriveDocx serves the Drive list endpoint plus the docx blocks /
|
||||
// media / export endpoints. blocksMode controls the blocks API behaviour:
|
||||
// - "ok": serve the given blocks
|
||||
// - "fail": HTTP 500 (missing scope) → exercises the export fallback
|
||||
// - "empty": serve only a page block → empty Markdown → export fallback
|
||||
//
|
||||
// The export trio is always registered so both fallback modes can complete.
|
||||
func fakeFeishuDriveDocx(t *testing.T, files []core.DriveFile, docToken string,
|
||||
blocks []core.DocxBlock, blocksMode string, mediaContent []byte,
|
||||
) (*httptest.Server, *core.Config) {
|
||||
t.Helper()
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/open-apis/auth/v3/tenant_access_token/internal", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, core.TokenResponse{ApiResponse: core.ApiResponse{Code: 0}, TenantAccessToken: "fake-token", Expire: 7200})
|
||||
})
|
||||
|
||||
// Drive file listing (single page).
|
||||
mux.HandleFunc("/open-apis/drive/v1/files", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, core.DriveFileListResponse{
|
||||
ApiResponse: core.ApiResponse{Code: 0},
|
||||
Data: core.DriveFileListData{Files: files},
|
||||
})
|
||||
})
|
||||
|
||||
// Blocks API for the given docx document.
|
||||
blocksPath := "/open-apis/docx/v1/documents/" + docToken + "/blocks"
|
||||
switch blocksMode {
|
||||
case "fail":
|
||||
mux.HandleFunc(blocksPath, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(`{"code":99991400,"msg":"insufficient scope"}`))
|
||||
})
|
||||
case "empty":
|
||||
mux.HandleFunc(blocksPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, core.DocxBlocksResponse{
|
||||
ApiResponse: core.ApiResponse{Code: 0},
|
||||
Data: core.DocxBlocksData{Items: []core.DocxBlock{{BlockID: "b1", BlockType: core.BlockTypePage}}},
|
||||
})
|
||||
})
|
||||
default: // "ok"
|
||||
mux.HandleFunc(blocksPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, core.DocxBlocksResponse{
|
||||
ApiResponse: core.ApiResponse{Code: 0},
|
||||
Data: core.DocxBlocksData{Items: blocks},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Media download for File/Image block tokens.
|
||||
mux.HandleFunc("/open-apis/drive/v1/medias/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/download") {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write(mediaContent)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
})
|
||||
|
||||
// Export trio for the fallback path.
|
||||
mux.HandleFunc("/open-apis/drive/v1/export_tasks", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, core.ExportTaskCreateResponse{
|
||||
ApiResponse: core.ApiResponse{Code: 0},
|
||||
Data: core.ExportTaskCreateData{Ticket: "ticket-drv"},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/open-apis/drive/v1/export_tasks/ticket-drv", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, core.ExportTaskStatusResponse{
|
||||
ApiResponse: core.ApiResponse{Code: 0},
|
||||
Data: core.ExportTaskStatusData{
|
||||
Result: core.ExportTaskResult{FileToken: "ft-export-drv", FileSize: 512, JobStatus: 0, FileName: "drive-fallback.docx"},
|
||||
},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/open-apis/drive/v1/export_tasks/file/ft-export-drv/download", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write([]byte("fake-drive-export-binary"))
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(mux)
|
||||
t.Cleanup(ts.Close)
|
||||
return ts, &core.Config{AppID: "test-app-id", AppSecret: "test-app-secret", BaseURL: ts.URL}
|
||||
}
|
||||
|
||||
func driveDocxBlocks(attToken, attName string) []core.DocxBlock {
|
||||
return []core.DocxBlock{
|
||||
{BlockID: "b1", BlockType: core.BlockTypePage},
|
||||
{BlockID: "b2", BlockType: core.BlockTypeText, Text: &core.BlockText{
|
||||
Elements: []core.TextElement{{TextRun: &core.TextRun{Content: "Hello drive"}}},
|
||||
}},
|
||||
{BlockID: "b3", BlockType: core.BlockTypeFile, File: &core.BlockFileRef{Token: attToken, Name: attName}},
|
||||
}
|
||||
}
|
||||
|
||||
const driveDocxFileToken = "fdoc1"
|
||||
|
||||
func driveDocxFile() core.DriveFile {
|
||||
return core.DriveFile{
|
||||
Token: driveDocxFileToken,
|
||||
Type: "docx",
|
||||
Name: "Drive Doc",
|
||||
ModifiedTime: "500",
|
||||
ParentToken: "folder1",
|
||||
URL: "https://example.feishu.cn/file/" + driveDocxFileToken,
|
||||
}
|
||||
}
|
||||
|
||||
// A drive docx goes through the blocks path: main Markdown item (external_id =
|
||||
// file token, channel = feishu_drive) plus the attachment sub-item.
|
||||
func TestDriveFetchStream_DocxBlocksMultiItem(t *testing.T) {
|
||||
t.Setenv("FEISHU_DOCX_PARSE_MODE", "blocks")
|
||||
const (
|
||||
attToken = "ft-drv-att"
|
||||
attName = "report.pdf"
|
||||
)
|
||||
attContent := bytes.Repeat([]byte("x"), core.MinAttachmentBytes+1)
|
||||
|
||||
_, cfg := fakeFeishuDriveDocx(t, []core.DriveFile{driveDocxFile()}, driveDocxFileToken,
|
||||
driveDocxBlocks(attToken, attName), "ok", attContent)
|
||||
|
||||
c := NewDriveConnector(core.RegionFeishuDrive)
|
||||
h := &recordingHandler{}
|
||||
_, err := c.FetchStream(context.Background(), makeDriveConfig(cfg, []string{"folder1"}, false), nil, h)
|
||||
if err != nil {
|
||||
t.Fatalf("FetchStream() error: %v", err)
|
||||
}
|
||||
|
||||
if len(h.emitted) != 2 {
|
||||
t.Fatalf("expected 2 emitted items (main doc + attachment), got %d: %+v", len(h.emitted), h.emitted)
|
||||
}
|
||||
|
||||
main := h.emitted[0]
|
||||
if main.ExternalID != driveDocxFileToken {
|
||||
t.Errorf("items[0].ExternalID = %q, want %q", main.ExternalID, driveDocxFileToken)
|
||||
}
|
||||
if main.ContentType != "text/markdown" {
|
||||
t.Errorf("items[0].ContentType = %q, want text/markdown", main.ContentType)
|
||||
}
|
||||
if !main.ReplacesSubtree {
|
||||
t.Errorf("items[0].ReplacesSubtree = false, want true")
|
||||
}
|
||||
if main.Metadata["channel"] != types.ChannelFeishuDrive {
|
||||
t.Errorf("items[0].Metadata[channel] = %q, want %q", main.Metadata["channel"], types.ChannelFeishuDrive)
|
||||
}
|
||||
if main.URL != "https://example.feishu.cn/file/"+driveDocxFileToken {
|
||||
t.Errorf("items[0].URL = %q, want drive file URL passthrough", main.URL)
|
||||
}
|
||||
if !strings.Contains(string(main.Content), "Hello drive") {
|
||||
t.Errorf("items[0].Content missing expected text; got %q", string(main.Content))
|
||||
}
|
||||
|
||||
att := h.emitted[1]
|
||||
wantAttID := driveDocxFileToken + "#file#" + attToken
|
||||
if att.ExternalID != wantAttID {
|
||||
t.Errorf("items[1].ExternalID = %q, want %q", att.ExternalID, wantAttID)
|
||||
}
|
||||
if att.Metadata["attachment"] != "true" {
|
||||
t.Errorf("items[1].Metadata[attachment] = %q, want \"true\"", att.Metadata["attachment"])
|
||||
}
|
||||
}
|
||||
|
||||
// Blocks API failure falls back to export: exactly one octet-stream item, no
|
||||
// ReplacesSubtree (must not sweep good prior children on a transient failure).
|
||||
func TestDriveFetchStream_DocxBlocksFailFallsBackToExport(t *testing.T) {
|
||||
t.Setenv("FEISHU_DOCX_PARSE_MODE", "blocks")
|
||||
_, cfg := fakeFeishuDriveDocx(t, []core.DriveFile{driveDocxFile()}, driveDocxFileToken, nil, "fail", nil)
|
||||
|
||||
c := NewDriveConnector(core.RegionFeishuDrive)
|
||||
h := &recordingHandler{}
|
||||
_, err := c.FetchStream(context.Background(), makeDriveConfig(cfg, []string{"folder1"}, false), nil, h)
|
||||
if err != nil {
|
||||
t.Fatalf("FetchStream() error: %v", err)
|
||||
}
|
||||
|
||||
if len(h.emitted) != 1 {
|
||||
t.Fatalf("expected 1 emitted item (export fallback), got %d: %+v", len(h.emitted), h.emitted)
|
||||
}
|
||||
item := h.emitted[0]
|
||||
if item.ExternalID != driveDocxFileToken {
|
||||
t.Errorf("item.ExternalID = %q, want %q", item.ExternalID, driveDocxFileToken)
|
||||
}
|
||||
if item.ContentType != "application/octet-stream" {
|
||||
t.Errorf("item.ContentType = %q, want application/octet-stream", item.ContentType)
|
||||
}
|
||||
if !strings.HasSuffix(item.FileName, ".docx") {
|
||||
t.Errorf("item.FileName = %q, want .docx suffix", item.FileName)
|
||||
}
|
||||
if item.ReplacesSubtree {
|
||||
t.Error("export-fallback item must not set ReplacesSubtree")
|
||||
}
|
||||
}
|
||||
|
||||
// Blocks rendering to empty Markdown also falls back to export (a blank page
|
||||
// would otherwise ingest as a login-gated URL core.Fetch and fail).
|
||||
func TestDriveFetchStream_DocxBlocksEmptyFallsBackToExport(t *testing.T) {
|
||||
t.Setenv("FEISHU_DOCX_PARSE_MODE", "blocks")
|
||||
_, cfg := fakeFeishuDriveDocx(t, []core.DriveFile{driveDocxFile()}, driveDocxFileToken, nil, "empty", nil)
|
||||
|
||||
c := NewDriveConnector(core.RegionFeishuDrive)
|
||||
h := &recordingHandler{}
|
||||
_, err := c.FetchStream(context.Background(), makeDriveConfig(cfg, []string{"folder1"}, false), nil, h)
|
||||
if err != nil {
|
||||
t.Fatalf("FetchStream() error: %v", err)
|
||||
}
|
||||
|
||||
if len(h.emitted) != 1 {
|
||||
t.Fatalf("expected 1 emitted item (export fallback), got %d: %+v", len(h.emitted), h.emitted)
|
||||
}
|
||||
item := h.emitted[0]
|
||||
if item.ContentType != "application/octet-stream" {
|
||||
t.Errorf("item.ContentType = %q, want application/octet-stream", item.ContentType)
|
||||
}
|
||||
if item.ReplacesSubtree {
|
||||
t.Error("export-fallback item must not set ReplacesSubtree")
|
||||
}
|
||||
}
|
||||
|
||||
// With multimodal disabled, an embedded image is not downloaded or emitted,
|
||||
// but its external_id is still in SubtreeKeep so a later toggle-on does not
|
||||
// sweep it, and toggling VLM off later does not delete previously OCR'd images.
|
||||
func TestDriveFetchStream_DocxImageMultimodalOff(t *testing.T) {
|
||||
t.Setenv("FEISHU_DOCX_PARSE_MODE", "blocks")
|
||||
const imgToken = "img-drv-1"
|
||||
blocks := []core.DocxBlock{
|
||||
{BlockID: "b1", BlockType: core.BlockTypePage},
|
||||
{BlockID: "b2", BlockType: core.BlockTypeText, Text: &core.BlockText{
|
||||
Elements: []core.TextElement{{TextRun: &core.TextRun{Content: "has image"}}},
|
||||
}},
|
||||
{BlockID: "b3", BlockType: core.BlockTypeImage, Image: &core.BlockTokenRef{Token: imgToken}},
|
||||
}
|
||||
_, cfg := fakeFeishuDriveDocx(t, []core.DriveFile{driveDocxFile()}, driveDocxFileToken, blocks, "ok", nil)
|
||||
|
||||
c := NewDriveConnector(core.RegionFeishuDrive)
|
||||
h := &recordingHandler{}
|
||||
_, err := c.FetchStream(context.Background(), makeDriveConfig(cfg, []string{"folder1"}, false), nil, h)
|
||||
if err != nil {
|
||||
t.Fatalf("FetchStream() error: %v", err)
|
||||
}
|
||||
|
||||
if len(h.emitted) != 1 {
|
||||
t.Fatalf("expected 1 emitted item (main only, image core.Skipped), got %d: %+v", len(h.emitted), h.emitted)
|
||||
}
|
||||
main := h.emitted[0]
|
||||
wantKeep := driveDocxFileToken + "#image#" + imgToken
|
||||
found := false
|
||||
for _, k := range main.SubtreeKeep {
|
||||
if k == wantKeep {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("SubtreeKeep = %v, want it to contain %q", main.SubtreeKeep, wantKeep)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user