diff --git a/internal/cli/schema_parameter_mapping_ledger.go b/internal/cli/schema_parameter_mapping_ledger.go index 1cc9c432..50229b4d 100644 --- a/internal/cli/schema_parameter_mapping_ledger.go +++ b/internal/cli/schema_parameter_mapping_ledger.go @@ -402,7 +402,9 @@ var reviewedSchemaParameterMappingExclusions = map[string]string{ "doc.get_document_content --start-block-id": "Runtime extension sends startBlockId for scoped JSONML reads, which is absent from the immutable pinned get_document_content metadata at its declared source revision.", "doc.get_document_content --tags": "Runtime extension sends tags for scoped JSONML reads, which is absent from the immutable pinned get_document_content metadata at its declared source revision.", "doc.get_document_style --node": "Reviewed unpinned adapter: doc.get_document_style has no singular pinned interface_ref; --node is a CLI wrapper input and does not publish a direct interface property.", + "doc.import_get --folder": "Composite recovery verification input; it is used only by the get_document_info readback after query_import_task completes.", "doc.import_get --task-id": "Reviewed unpinned adapter: doc.import_get has no singular pinned interface_ref; --task-id is a CLI wrapper input and does not publish a direct interface property.", + "doc.import_get --workspace": "Composite recovery verification input; it is used only by the get_document_info readback after query_import_task completes.", "doc.insert_document_block --fix-jsonml": "local JSONML normalization control", "doc.insert_document_block --heading": "aggregate convenience input used to build element", "doc.insert_document_block --level": "aggregate convenience input used to build element", diff --git a/internal/helpers/doc.go b/internal/helpers/doc.go index a4cf3cb9..1702ff7b 100644 --- a/internal/helpers/doc.go +++ b/internal/helpers/doc.go @@ -4316,13 +4316,15 @@ CLI 内部自动完成全部流程: Short: "查询导入任务结果(手动兜底)", Long: `根据 taskId 查询文档导入任务的执行结果。 通常不需要手动调用,dws doc import 会自动完成轮询。 -仅在导入命令超时或中断后,用于手动查询任务状态。 +仅在导入命令超时或中断后,用于手动查询任务状态。必须同时提供原导入 +目标(--folder 或 --workspace),以便 completed 后回读验证真实落点;优先 +直接复制超时结果中的完整 next_command。 任务状态: processing 转换中 completed 导入成功,返回 documentUrl failed 导入失败`, - Example: ` dws doc import get --task-id `, + Example: ` dws doc import get --task-id --workspace `, RunE: func(cmd *cobra.Command, _ []string) error { return runImportGetCommand(cmd, docImportFlowConfig()) }, @@ -4349,13 +4351,16 @@ CLI 内部自动完成全部流程: }, Selection: contract.SelectionSpec{ AgentSummary: "根据 taskId 查询文档导入任务的执行结果", - UseWhen: []string{"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"}, + UseWhen: []string{"已有 doc import 超时结果及其完整 next_command,需要续查同一 taskId 并验证原 folder/workspace 落点时"}, AvoidWhen: []string{"发起导入用 doc import(若入口可用);不要用本命令代替导入"}, - Examples: []string{"dws doc import get --task-id --format json"}, + Examples: []string{"dws doc import get --task-id --workspace --format json"}, }, }, }) importGetCmd.Flags().String("task-id", "", "导入任务 ID (必填)") + importGetCmd.Flags().String("folder", "", "原导入目标文件夹 ID 或 URL(与 --workspace 至少提供一个,用于落点验证)") + importGetCmd.Flags().String("workspace", "", "原导入目标知识库 ID 或 URL(与 --folder 至少提供一个,用于落点验证)") + importGetCmd.MarkFlagsOneRequired("folder", "workspace") importCmd.AddCommand(importGetCmd) newHybridGroupCommand(importCmd) diff --git a/internal/helpers/doc_command_edges_test.go b/internal/helpers/doc_command_edges_test.go index 1d2918c2..85ca5282 100644 --- a/internal/helpers/doc_command_edges_test.go +++ b/internal/helpers/doc_command_edges_test.go @@ -683,7 +683,7 @@ func TestCrossPlatformCoverageDocExportImportCommandEdges(t *testing.T) { {"failed empty", []scriptedToolStep{{text: `{"status":"failed"}`}}, false}, } { t.Run("import get "+tc.name, func(t *testing.T) { - _ = run(t, tc.steps, tc.dry, "import", "get", "--task-id=task") + _ = run(t, tc.steps, tc.dry, "import", "get", "--task-id=task", "--workspace=workspace") }) } } diff --git a/internal/helpers/doc_import_target_test.go b/internal/helpers/doc_import_target_test.go index 7de9b1b5..1a9620d1 100644 --- a/internal/helpers/doc_import_target_test.go +++ b/internal/helpers/doc_import_target_test.go @@ -19,6 +19,7 @@ import ( "encoding/json" "errors" "os" + "strings" "testing" "time" @@ -252,3 +253,108 @@ func TestCrossPlatformCoverageDocImportTargetDefensiveBranches(t *testing.T) { verifyFailure(t, preparedImportFile{}, `{"nodeId":"other"}`, "https://alidocs.dingtalk.com/i/nodes/node-1") verifyFailure(t, preparedImportFile{workspace: "space-1"}, `{"nodeId":"node-1","workspaceId":"wrong"}`, "https://alidocs.dingtalk.com/i/nodes/node-1") } + +func TestCrossPlatformCoverageDocImportGetVerifiesOriginalTarget(t *testing.T) { + previousDeps := deps + previousArgs := os.Args + t.Cleanup(func() { + deps = previousDeps + os.Args = previousArgs + }) + os.Args = []string{"dws", "doc"} + + caller := &sheetImportCaller{responses: map[string][]string{ + "query_import_task": {`{"status":"completed","documentUrl":"https://alidocs.dingtalk.com/i/nodes/node-2"}`}, + "get_document_info": {`{"result":{"nodeId":"node-2","workspaceId":"space-2","name":"report"}}`}, + }} + InitDeps(caller) + var output bytes.Buffer + deps.Out.w = &output + deps.Out.errW = &output + + cmd := &cobra.Command{Use: "get"} + cmd.Flags().String("task-id", "task-2", "") + cmd.Flags().String("folder", "", "") + cmd.Flags().String("workspace", "space-2", "") + if err := runImportGetCommand(cmd, docImportFlowConfig()); err != nil { + t.Fatalf("doc import get: %v", err) + } + if len(caller.calls) != 2 || caller.calls[0].tool != "query_import_task" || caller.calls[1].tool != "get_document_info" { + t.Fatalf("calls = %#v", caller.calls) + } + var result map[string]any + if err := json.Unmarshal(output.Bytes(), &result); err != nil { + t.Fatalf("decode output: %v\n%s", err, output.String()) + } + if result["nodeId"] != "node-2" || result["verified"] != true { + t.Fatalf("result = %#v", result) + } + target, _ := result["target"].(map[string]any) + if target["source"] != "workspace_flag" || target["workspaceId"] != "space-2" { + t.Fatalf("target = %#v", target) + } +} + +func TestCrossPlatformCoverageDocImportGetRequiresOriginalTarget(t *testing.T) { + previousDeps := deps + t.Cleanup(func() { deps = previousDeps }) + caller := &sheetImportCaller{responses: map[string][]string{}} + InitDeps(caller) + cmd := &cobra.Command{Use: "get"} + cmd.Flags().String("task-id", "task-2", "") + cmd.Flags().String("folder", "", "") + cmd.Flags().String("workspace", "", "") + if err := runImportGetCommand(cmd, docImportFlowConfig()); err == nil || !strings.Contains(err.Error(), "完整 next_command") { + t.Fatalf("missing target error = %v", err) + } + if len(caller.calls) != 0 { + t.Fatalf("missing target reached MCP: %#v", caller.calls) + } +} + +func TestCrossPlatformCoverageDocImportGetDryRunIncludesTarget(t *testing.T) { + previousDeps := deps + t.Cleanup(func() { deps = previousDeps }) + caller := &sheetImportCaller{dryRun: true} + InitDeps(caller) + var output bytes.Buffer + deps.Out.w = &output + + cmd := &cobra.Command{Use: "get"} + cmd.Flags().String("task-id", "task-2", "") + cmd.Flags().String("folder", "", "") + cmd.Flags().String("workspace", "space-2", "") + if err := runImportGetCommand(cmd, docImportFlowConfig()); err != nil { + t.Fatalf("doc import get dry-run: %v", err) + } + if len(caller.calls) != 0 { + t.Fatalf("dry-run reached MCP: %#v", caller.calls) + } + + var result map[string]any + if err := json.Unmarshal(output.Bytes(), &result); err != nil { + t.Fatalf("decode output: %v\n%s", err, output.String()) + } + if result["dry_run"] != true || result["executed"] != false { + t.Fatalf("result = %#v", result) + } + target, _ := result["target"].(map[string]any) + if target["source"] != "workspace_flag" || target["workspaceId"] != "space-2" { + t.Fatalf("target = %#v", target) + } +} + +func TestCrossPlatformCoverageDocImportRecoveryCommandCarriesEveryTarget(t *testing.T) { + got := importRecoveryCommand(docImportFlowConfig(), "task-1", preparedImportFile{ + folder: "folder;unsafe", workspace: "https://alidocs.test/space?id=1&kind=doc", + }) + for _, want := range []string{ + "dws doc import get --task-id task-1", + "--folder 'folder;unsafe'", + "--workspace 'https://alidocs.test/space?id=1&kind=doc'", + } { + if !strings.Contains(got, want) { + t.Fatalf("recovery command = %q, want %q", got, want) + } + } +} diff --git a/internal/helpers/import_flow.go b/internal/helpers/import_flow.go index 91a767e7..6deca7d2 100644 --- a/internal/helpers/import_flow.go +++ b/internal/helpers/import_flow.go @@ -328,7 +328,7 @@ func findImportedDocumentInfo(value any) (map[string]any, bool) { if !ok { return nil, false } - if importString(object, "nodeId", "folderId", "workspaceId") != "" { + if importString(object, "nodeId", "fileId", "dentryUuid", "folderId", "workspaceId") != "" { return object, true } for _, key := range []string{"result", "data", "documentInfo", "document", "doc", "file"} { @@ -372,6 +372,10 @@ func verifyImportedDocumentPlacement(ctx context.Context, file preparedImportFil if nodeID == "" { return "", nil, docImportPlacementError(file, taskID, "", nil, fmt.Errorf("导入结果缺少可解析的 documentUrl")) } + return verifyImportedNodePlacement(ctx, file, taskID, nodeID) +} + +func verifyImportedNodePlacement(ctx context.Context, file preparedImportFile, taskID, nodeID string) (string, map[string]any, error) { text, err := callMCPToolReturnTextOnServer(ctx, "doc", "get_document_info", map[string]any{"nodeId": nodeID}) if err != nil { return nodeID, nil, docImportPlacementError(file, taskID, nodeID, nil, err) @@ -381,7 +385,7 @@ func verifyImportedDocumentPlacement(ctx context.Context, file preparedImportFil return nodeID, nil, docImportPlacementError(file, taskID, nodeID, nil, err) } - observedNodeID := importString(info, "nodeId") + observedNodeID := importString(info, "nodeId", "fileId", "dentryUuid", "id") if observedNodeID != "" && observedNodeID != nodeID { return nodeID, info, docImportPlacementError(file, taskID, nodeID, info, fmt.Errorf("回读 nodeId=%s,与导入结果 nodeId=%s 不一致", observedNodeID, nodeID)) @@ -429,16 +433,18 @@ func importTargetSummary(file preparedImportFile) map[string]any { func docImportPlacementError(file preparedImportFile, taskID, nodeID string, observed map[string]any, cause error) error { details := map[string]any{ "status": "partial_success", - "taskId": taskID, "nodeId": nodeID, "target": importTargetSummary(file), "verified": false, } + if taskID != "" { + details["taskId"] = taskID + } if observed != nil { details["observed"] = compactImportedDocumentInfo(observed, nodeID) } return apperrors.NewAPI( - "导入任务已经完成,但目标落点回读验证失败;为避免重复创建,请先按 nodeId 检查文档", + "导入或上传已经完成,但目标落点回读验证失败;为避免重复创建,请先按 nodeId 检查文档", apperrors.WithOperation("doc.import"), apperrors.WithReason("doc_import_placement_unverified"), apperrors.WithFailureStage("verify_placement"), @@ -469,7 +475,7 @@ func runImportUploadFallback(cmd *cobra.Command, cfg importFlowConfig, file prep label = "无扩展名" } deps.Out.PrintWarning(fmt.Sprintf( - "%s 文件不支持转换为在线文档(支持: %s),已自动改走文件上传链路,以原文件形式存入 --folder/--workspace 指定的目标位置;如需在线文档,请先将内容转换为 md 后重新执行 doc import;上传到钉盘请用 dws drive upload", + "%s 文件不支持转换为在线文档(支持: %s),已自动改走文件上传链路,以原文件形式存入解析出的文档目标位置;如需在线文档,请先将内容转换为 md 后重新执行 doc import;上传到钉盘请用 dws drive upload", label, cfg.supportedFormatsText)) // prepareImportFile 的 name 去掉了扩展名;上传保留原始文件名形态 @@ -523,7 +529,14 @@ func runImportUploadFallback(cmd *cobra.Command, cfg importFlowConfig, file prep if err != nil { return err } - return deps.Out.PrintJSON(map[string]any{ + var verification map[string]any + if cfg.verifyPlacement { + _, verification, err = verifyImportedNodePlacement(ctx, file, "", dentryID) + if err != nil { + return err + } + } + result := map[string]any{ "success": true, "operation": "上传文件到钉钉文档", "requested_operation": cfg.operation, @@ -533,7 +546,13 @@ func runImportUploadFallback(cmd *cobra.Command, cfg importFlowConfig, file prep "format": file.extension, "dentry_id": dentryID, "result": commit, - }) + } + if cfg.verifyPlacement { + result["verified"] = true + result["target"] = importTargetSummary(file) + result["verification"] = verification + } + return deps.Out.PrintJSON(result) } // uploadCommitIDKeys 是 commit_uploaded_file 响应中可作为文件标识的字段, @@ -569,13 +588,13 @@ func runImportCommand(cmd *cobra.Command, args []string, cfg importFlowConfig) e if err != nil { return err } - // 非回退配置的白名单外文件已在 prepareImportFile 中按基线顺序拒绝 - if cfg.uploadFallback && !cfg.supportedFormats[file.extension] { - return runImportUploadFallback(cmd, cfg, file) - } + uploadFallback := cfg.uploadFallback && !cfg.supportedFormats[file.extension] jsonMode := deps.Caller.Format() == "json" if deps.Caller.DryRun() { + if uploadFallback { + return runImportUploadFallback(cmd, cfg, file) + } if jsonMode { return deps.Out.PrintJSON(map[string]any{ "dry_run": true, @@ -605,6 +624,11 @@ func runImportCommand(cmd *cobra.Command, args []string, cfg importFlowConfig) e return err } } + // 实际执行时,白名单外格式也必须先完成与在线转换路径相同的目标解析, + // 再进入上传回退;这样无显式目标时仍落到“我的文档”,且可执行同一套回读验证。 + if uploadFallback { + return runImportUploadFallback(cmd, cfg, file) + } if !jsonMode { deps.Out.PrintInfo("[1/4] 创建导入会话...") @@ -691,10 +715,10 @@ func runImportCommand(cmd *cobra.Command, args []string, cfg importFlowConfig) e "timed_out": true, "taskId": taskID, "status": "processing", - "next_command": fmt.Sprintf(cfg.nextCommand, taskID), + "next_command": importRecoveryCommand(cfg, taskID, file), }) } - return fmt.Errorf("%s,请稍后使用 %s 手动查询", timeoutErr.Error(), fmt.Sprintf(cfg.nextCommand, taskID)) + return fmt.Errorf("%s,请稍后使用 %s 手动查询", timeoutErr.Error(), importRecoveryCommand(cfg, taskID, file)) } documentURL, _ := result["documentUrl"].(string) @@ -730,20 +754,50 @@ func runImportCommand(cmd *cobra.Command, args []string, cfg importFlowConfig) e return deps.Out.PrintJSON(finalResult) } +func importRecoveryCommand(cfg importFlowConfig, taskID string, file preparedImportFile) string { + command := fmt.Sprintf(cfg.nextCommand, ShellQuoteArg(taskID)) + if !cfg.verifyPlacement { + return command + } + if file.folder != "" { + command += " --folder " + ShellQuoteArg(file.folder) + } + if file.workspace != "" { + command += " --workspace " + ShellQuoteArg(file.workspace) + } + return command +} + func runImportGetCommand(cmd *cobra.Command, cfg importFlowConfig) error { taskID := mustGetFlag(cmd, "task-id") if taskID == "" { return fmt.Errorf("flag --task-id is required") } + target := preparedImportFile{ + folder: importFlagValue(cmd, cfg.folderFlags...), + workspace: importFlagValue(cmd, cfg.workspaceFlags...), + } + if target.folder != "" { + target.target = "folder_flag" + } else if target.workspace != "" { + target.target = "workspace_flag" + } + if cfg.verifyPlacement && target.folder == "" && target.workspace == "" { + return fmt.Errorf("doc import get 需要通过 --folder 或 --workspace 提供原导入目标;请直接使用 doc import 超时结果中的完整 next_command") + } if deps.Caller.DryRun() { if deps.Caller.Format() == "json" { - return deps.Out.PrintJSON(map[string]any{ + preview := map[string]any{ "dry_run": true, "executed": false, "preview_kind": "plan", "operation": cfg.queryOperation, "taskId": taskID, - }) + } + if cfg.verifyPlacement { + preview["target"] = importTargetSummary(target) + } + return deps.Out.PrintJSON(preview) } deps.Out.PrintKeyValue("操作", cfg.queryOperation) deps.Out.PrintKeyValue("任务ID", taskID) @@ -767,9 +821,20 @@ func runImportGetCommand(cmd *cobra.Command, cfg importFlowConfig) error { status, _ := result["status"].(string) message, _ := result["message"].(string) if strings.EqualFold(status, "completed") { + documentURL, _ := result["documentUrl"].(string) + nodeID := extractNodeIDFromDocURL(documentURL) + if cfg.verifyPlacement { + var verification map[string]any + nodeID, verification, err = verifyImportedDocumentPlacement(ctx, target, taskID, documentURL) + if err != nil { + return err + } + result["verified"] = true + result["target"] = importTargetSummary(target) + result["verification"] = verification + } if cfg.includeNodeID { - documentURL, _ := result["documentUrl"].(string) - result["nodeId"] = extractNodeIDFromDocURL(documentURL) + result["nodeId"] = nodeID } return deps.Out.PrintJSON(result) } diff --git a/internal/helpers/import_flow_coverage_more_test.go b/internal/helpers/import_flow_coverage_more_test.go index c7bda608..633b309d 100644 --- a/internal/helpers/import_flow_coverage_more_test.go +++ b/internal/helpers/import_flow_coverage_more_test.go @@ -77,7 +77,7 @@ func TestCrossPlatformCoverageImportFlowRemainingBranches(t *testing.T) { cfg.poll.interval = func(int) time.Duration { return 0 } cfg.poll.wait = func(context.Context, time.Duration) error { return nil } err := runImportCommand(importCoverageCommand(t, writeImportFixture(t, "md")), nil, cfg) - if err == nil || !strings.Contains(err.Error(), "手动查询") { + if err == nil || !strings.Contains(err.Error(), "手动查询") || !strings.Contains(err.Error(), "--workspace my-space") { t.Fatalf("runImportCommand() error = %v, want manual-query timeout", err) } }) @@ -90,6 +90,8 @@ func TestCrossPlatformCoverageImportFlowRemainingBranches(t *testing.T) { installScriptedCaller(t, &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"status":"processing"}`}}}) cmd := &cobra.Command{Use: "get"} cmd.Flags().String("task-id", "task-1", "") + cmd.Flags().String("folder", "", "") + cmd.Flags().String("workspace", "workspace-1", "") if err := runImportGetCommand(cmd, docImportFlowConfig()); err != nil { t.Fatalf("runImportGetCommand() error = %v", err) } diff --git a/internal/helpers/import_html_redirect_test.go b/internal/helpers/import_html_redirect_test.go index 772b2b09..a0b182ae 100644 --- a/internal/helpers/import_html_redirect_test.go +++ b/internal/helpers/import_html_redirect_test.go @@ -23,6 +23,7 @@ import ( "strings" "testing" + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/spf13/cobra" ) @@ -56,6 +57,7 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { uploadSteps := []scriptedToolStep{ {text: `{"resourceUrl":"https://upload.example.test/object","uploadKey":"key-1"}`}, {text: `{"dentryUuid":"node-1","name":"sales.html"}`}, + {text: `{"nodeId":"node-1","workspaceId":"ws-1","folderId":"folder-abc","name":"sales.html"}`}, } t.Run("html upload fallback emits marked json without legacy warnings", func(t *testing.T) { @@ -74,20 +76,20 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { if err := runImportCommand(cmd, nil, docImportFlowConfig()); err != nil { t.Fatalf("runImportCommand() error = %v, want upload fallback success", err) } - if caller.calls != 2 || caller.tool != "commit_uploaded_file" { - t.Fatalf("fallback calls = %d last tool = %q, want 2 calls ending in commit_uploaded_file", caller.calls, caller.tool) + if caller.calls != 3 || caller.tool != "get_document_info" { + t.Fatalf("fallback calls = %d last tool = %q, want readback after commit", caller.calls, caller.tool) } - if got := caller.args["workspaceId"]; got != "ws-1" { + if got := caller.argsLog[1]["workspaceId"]; got != "ws-1" { t.Fatalf("commit workspaceId = %v, want ws-1", got) } - if got := caller.args["name"]; got != "sales.html" { + if got := caller.argsLog[1]["name"]; got != "sales.html" { t.Fatalf("commit name = %v, want original file name with extension", got) } var payload map[string]any if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil { t.Fatalf("fallback result must be one JSON document: %v\n%s", err, stdout.String()) } - if payload["success"] != true || payload["fallback"] != "upload" || payload["converted"] != false { + if payload["success"] != true || payload["fallback"] != "upload" || payload["converted"] != false || payload["verified"] != true { t.Fatalf("fallback markers missing: %#v", payload) } if payload["dentry_id"] != "node-1" { @@ -104,6 +106,63 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { } }) + t.Run("fallback resolves and verifies the default personal workspace", func(t *testing.T) { + caller := &scriptedToolCaller{format: "json", steps: []scriptedToolStep{ + {text: `{"wikiSpaces":[{"workspaceId":"my-space"}]}`}, + {text: `{"resourceUrl":"https://upload.example.test/object","uploadKey":"key-1"}`}, + {text: `{"dentryUuid":"node-default","name":"sales.pdf"}`}, + {text: `{"nodeId":"node-default","workspaceId":"my-space","name":"sales.pdf"}`}, + }} + installScriptedCaller(t, caller) + var stdout bytes.Buffer + deps.Out.w = &stdout + SetHTTPPutFile(func(context.Context, string, map[string]string, string, int64) error { return nil }) + t.Cleanup(func() { SetHTTPPutFile(nil) }) + + cmd := htmlFallbackCommand(t, writeImportFixture(t, "pdf")) + if err := runImportCommand(cmd, nil, docImportFlowConfig()); err != nil { + t.Fatalf("runImportCommand() error = %v", err) + } + if strings.Join(caller.toolLog, ",") != "list_wikiSpaces,get_file_upload_info,commit_uploaded_file,get_document_info" { + t.Fatalf("fallback calls = %#v", caller.toolLog) + } + if got := caller.argsLog[2]["workspaceId"]; got != "my-space" { + t.Fatalf("default commit workspaceId = %#v", got) + } + var payload map[string]any + if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil { + t.Fatal(err) + } + target, _ := payload["target"].(map[string]any) + if payload["verified"] != true || target["source"] != "default_personal_workspace" || target["workspaceId"] != "my-space" { + t.Fatalf("fallback result = %#v", payload) + } + }) + + t.Run("fallback does not report success when placement readback mismatches", func(t *testing.T) { + caller := &scriptedToolCaller{format: "json", steps: []scriptedToolStep{ + {text: `{"resourceUrl":"https://upload.example.test/object","uploadKey":"key-1"}`}, + {text: `{"dentryUuid":"node-mismatch"}`}, + {text: `{"nodeId":"node-mismatch","workspaceId":"wrong-space"}`}, + }} + installScriptedCaller(t, caller) + var stdout bytes.Buffer + deps.Out.w = &stdout + SetHTTPPutFile(func(context.Context, string, map[string]string, string, int64) error { return nil }) + t.Cleanup(func() { SetHTTPPutFile(nil) }) + + cmd := htmlFallbackCommand(t, writeImportFixture(t, "pdf")) + _ = cmd.Flags().Set("workspace", "expected-space") + err := runImportCommand(cmd, nil, docImportFlowConfig()) + var structured *apperrors.Error + if !errors.As(err, &structured) || structured.Reason != "doc_import_placement_unverified" { + t.Fatalf("placement mismatch error = %#v", err) + } + if stdout.Len() != 0 { + t.Fatalf("placement mismatch emitted success output: %s", stdout.String()) + } + }) + t.Run("json dry run stays a single json document", func(t *testing.T) { caller := &scriptedToolCaller{format: "json", dry: true} installScriptedCaller(t, caller) @@ -134,11 +193,12 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { t.Cleanup(func() { SetHTTPPutFile(nil) }) cmd := htmlFallbackCommand(t, writeImportFixture(t, ext)) + _ = cmd.Flags().Set("workspace", "ws-1") if err := runImportCommand(cmd, nil, docImportFlowConfig()); err != nil { t.Fatalf("runImportCommand(%s) error = %v, want upload fallback success", ext, err) } - if caller.tool != "commit_uploaded_file" { - t.Fatalf("%s last tool = %q, want commit_uploaded_file", ext, caller.tool) + if caller.tool != "get_document_info" { + t.Fatalf("%s last tool = %q, want get_document_info", ext, caller.tool) } } }) @@ -151,11 +211,12 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { path := writeImportFixture(t, "HTM") cmd := htmlFallbackCommand(t, "") + _ = cmd.Flags().Set("workspace", "ws-1") if err := runImportCommand(cmd, []string{path}, docImportFlowConfig()); err != nil { t.Fatalf("runImportCommand() error = %v, want upload fallback success", err) } - if caller.tool != "commit_uploaded_file" { - t.Fatalf("last tool = %q, want commit_uploaded_file", caller.tool) + if caller.tool != "get_document_info" { + t.Fatalf("last tool = %q, want get_document_info", caller.tool) } }) @@ -172,7 +233,7 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { if err := runImportCommand(cmd, nil, docImportFlowConfig()); err != nil { t.Fatalf("runImportCommand() error = %v, want upload fallback success", err) } - if got := caller.args["folderId"]; got != "folder-abc" { + if got := caller.argsLog[1]["folderId"]; got != "folder-abc" { t.Fatalf("commit folderId = %v, want folder-abc from --folder-id alias", got) } }) @@ -190,6 +251,7 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { t.Fatal(err) } cmd := htmlFallbackCommand(t, noExt) + _ = cmd.Flags().Set("workspace", "ws-1") if err := runImportCommand(cmd, nil, docImportFlowConfig()); err != nil { t.Fatalf("runImportCommand() error = %v, want upload fallback success", err) } @@ -298,6 +360,7 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { t.Cleanup(func() { SetHTTPPutFile(nil) }) cmd := htmlFallbackCommand(t, writeImportFixture(t, "html")) + _ = cmd.Flags().Set("workspace", "ws-1") err := runImportCommand(cmd, nil, docImportFlowConfig()) if err == nil || !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("runImportCommand() error = %v, want %q", err, tc.wantErr) @@ -326,6 +389,7 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { t.Cleanup(func() { SetHTTPPutFile(nil) }) cmd := htmlFallbackCommand(t, writeImportFixture(t, "html")) + _ = cmd.Flags().Set("workspace", "ws-1") err := runImportCommand(cmd, nil, docImportFlowConfig()) if err == nil || !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("runImportCommand() error = %v, want %q", err, tc.wantErr) @@ -338,6 +402,7 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { caller := &scriptedToolCaller{format: "json", steps: []scriptedToolStep{ {text: `{"resourceUrl":"https://upload.example.test/object","uploadKey":"key-1"}`}, {text: `{"result":{"dentryUuid":"nested-node-9"}}`}, + {text: `{"fileId":"nested-node-9","workspaceId":"ws-1"}`}, }} installScriptedCaller(t, caller) var stdout bytes.Buffer @@ -346,6 +411,7 @@ func TestCrossPlatformCoverageDocImportHTMLUploadRedirect(t *testing.T) { t.Cleanup(func() { SetHTTPPutFile(nil) }) cmd := htmlFallbackCommand(t, writeImportFixture(t, "html")) + _ = cmd.Flags().Set("workspace", "ws-1") if err := runImportCommand(cmd, nil, docImportFlowConfig()); err != nil { t.Fatalf("runImportCommand() error = %v", err) } diff --git a/internal/helpers/wukong_sync_commands_test.go b/internal/helpers/wukong_sync_commands_test.go index 6fd1c379..8a3b1135 100644 --- a/internal/helpers/wukong_sync_commands_test.go +++ b/internal/helpers/wukong_sync_commands_test.go @@ -93,7 +93,7 @@ func TestWukongSyncDocCommands(t *testing.T) { requireWukongSyncFlags(t, importCmd, "file", "folder", "workspace", "name", "folder-id", "workspace-id") importGetCmd := requireWukongSyncCommand(t, root, "import", "get") - requireWukongSyncFlags(t, importGetCmd, "task-id") + requireWukongSyncFlags(t, importGetCmd, "task-id", "folder", "workspace") } func TestWukongSyncSheetCommands(t *testing.T) {