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

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
@@ -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)
}
}