From 2683a0afa2e13ae6d099f7e7351f83283c3fb7f1 Mon Sep 17 00:00:00 2001 From: ZoOL Date: Fri, 10 Jul 2026 12:22:07 +0800 Subject: [PATCH 1/3] refactor!: fix P9 interface-shape lies in workflow and analytics (#350) Workflow approve/reject/resubmit helpers returned ApprovalTaskActionResult with success always true after discarding the real response. Return SDKResult<()> instead so success/failure is only expressed by Result. Analytics deleted search/v2 query and user runtime stubs that always returned Err with dead setters (no verified Feishu endpoints; aligns with Platform/user Service::new fixes already landed in #373/#360. --- CHANGELOG.md | 18 +++ Cargo.lock | 1 + crates/openlark-analytics/src/lib.rs | 2 +- .../src/search/search/v2.rs | 2 - .../src/search/search/v2/query.rs | 149 ------------------ .../src/search/search/v2/user.rs | 95 ----------- crates/openlark-analytics/src/service.rs | 20 +-- crates/openlark-client/src/client.rs | 2 +- crates/openlark-workflow/Cargo.toml | 1 + crates/openlark-workflow/src/service.rs | 140 ++++++++++++---- docs/RUNTIME_API_STUB_AUDIT.md | 40 ++--- ...ommunication-workflow-helper-boundaries.md | 4 +- .../communication_workflows.rs | 4 +- examples/workflow_api_example.rs | 4 +- .../no-deprecated-compat-accessors/spec.md | 6 +- 15 files changed, 170 insertions(+), 318 deletions(-) delete mode 100644 crates/openlark-analytics/src/search/search/v2/query.rs delete mode 100644 crates/openlark-analytics/src/search/search/v2/user.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cc59a9032..01fd6c8e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking +- **#350 P9 接口形状撒谎修正(workflow + analytics;platform/user 已先行)**: + - **workflow**:`approve_task`/`reject_task`/`resubmit_task` 原丢弃真实响应并恒返回 + `ApprovalTaskActionResult { success: true }`(`success: false` 永不达)。改为 + `SDKResult<()>`——成功/失败只由 `Result` 表达;删除 `ApprovalTaskActionResult`。 + 飞书 approval v4 同意/拒绝/重提响应 data 为空,与 `()` 一致。**迁移**: + `let r = service.approve_task(...).await?; r.success` → `service.approve_task(...).await?`。 + - **analytics**: + 1. 删除 `search/v2/query.rs` 与 `search/v2/user.rs` 恒 `Err` runtime stub + (`QueryApi`/`UserSearchApi`/`SearchRequest`/`SuggestRequest`/`SearchUserRequest`)。 + 无已验证飞书端点(与 #2fab71234 / #108 约束一致:不发明未验证端点);setter 死值 + + `execute()` 恒失败是接口撒谎。与 #308 删除 `Search`/`SearchV2` 门面死链同向收口。 + **迁移**:改用已实现的 `doc_wiki`/`schema`/`app`/`message`/`data_source` leaf builder。 + 2. `AnalyticsService::new` 误导签名 `SDKResult` 但函数体永远 `Ok(...)` → 改为 `Self` + (同 platform #373 / user #360)。**迁移**:`AnalyticsService::new(config)?` / client + facade 去 `?`。 + - **platform / user(已合入)**:`PlatformService::new`(#373)、`UserService::new`(#360) + 误导 `SDKResult` → `Self`,不在本变更重复。 + - **meeting_room 17 叶 `execute()` 返回类型 `Value` → typed Response**(#349): `meeting_room/{building,room,country,district,freebusy,instance,summary}` 全部 `execute()` / `execute_with_options()` 从 `SDKResult` 改为 typed diff --git a/Cargo.lock b/Cargo.lock index d39708c1b..ccbff632b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1894,6 +1894,7 @@ dependencies = [ "serde_json", "tokio", "tracing", + "wiremock", ] [[package]] diff --git a/crates/openlark-analytics/src/lib.rs b/crates/openlark-analytics/src/lib.rs index 47c41521d..ea3291ba8 100644 --- a/crates/openlark-analytics/src/lib.rs +++ b/crates/openlark-analytics/src/lib.rs @@ -25,7 +25,7 @@ //! .app_secret("app_secret") //! .build(); //! -//! let _analytics_service = AnalyticsService::new(config)?; +//! let _analytics_service = AnalyticsService::new(config); //! //! # Ok(()) //! # } diff --git a/crates/openlark-analytics/src/search/search/v2.rs b/crates/openlark-analytics/src/search/search/v2.rs index 882f0140f..dcf88a3d4 100644 --- a/crates/openlark-analytics/src/search/search/v2.rs +++ b/crates/openlark-analytics/src/search/search/v2.rs @@ -7,6 +7,4 @@ pub mod app; pub mod data_source; pub mod doc_wiki; pub mod message; -pub mod query; pub mod schema; -pub mod user; diff --git a/crates/openlark-analytics/src/search/search/v2/query.rs b/crates/openlark-analytics/src/search/search/v2/query.rs deleted file mode 100644 index dcd66e887..000000000 --- a/crates/openlark-analytics/src/search/search/v2/query.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! 查询搜索 API -//! -//! 当前仍是 runtime stub。 -//! -//! 该模块历史上暴露了查询 / 搜索建议入口,但并没有对应的已接线服务端实现。 -//! 为避免继续返回占位 JSON,本模块现在会显式返回未接线错误。 - -use crate::AnalyticsConfig; -use openlark_core::{SDKResult, error::business_error, req_option::RequestOption}; -use std::sync::Arc; - -/// 查询搜索 API -#[derive(Debug, Clone)] -pub struct QueryApi { - config: Arc, -} - -impl QueryApi { - /// 创建新的查询搜索 facade。 - pub fn new(config: Arc) -> Self { - Self { config } - } - - /// 搜索 - pub fn search(&self) -> SearchRequest { - SearchRequest::new(self.config.clone()) - } - - /// 获取搜索建议 - pub fn suggest(&self) -> SuggestRequest { - SuggestRequest::new(self.config.clone()) - } -} - -/// 搜索请求 -pub struct SearchRequest { - // reserved:待装访问器/execute(见 #274,不完整脚手架) - _config: Arc, - search_term: Option, - search_type: Option, - page_size: Option, -} - -impl SearchRequest { - fn new(config: Arc) -> Self { - Self { - _config: config, - search_term: None, - search_type: None, - page_size: None, - } - } - - /// 设置搜索词 - pub fn search_term(mut self, term: impl Into) -> Self { - self.search_term = Some(term.into()); - self - } - - /// 设置搜索类型 - pub fn search_type(mut self, search_type: impl Into) -> Self { - self.search_type = Some(search_type.into()); - self - } - - /// 设置页面大小 - pub fn page_size(mut self, size: u32) -> Self { - self.page_size = Some(size); - self - } - - fn unsupported(operation: &str) -> openlark_core::error::CoreError { - business_error(format!( - "{operation}: openlark-analytics 尚未接入该 search runtime API,请改用已实现的 doc_wiki/schema/app/message 路径" - )) - } - - /// 执行请求。 - pub async fn execute(self) -> SDKResult { - self.execute_with_options(RequestOption::default()).await - } - - /// 执行请求并传入请求选项。 - pub async fn execute_with_options( - self, - _option: RequestOption, - ) -> SDKResult { - Err(Self::unsupported("query.search")) - } -} - -/// 搜索建议请求 -pub struct SuggestRequest { - // reserved:待装访问器/execute(见 #274,不完整脚手架) - _config: Arc, - query: Option, -} - -impl SuggestRequest { - fn new(config: Arc) -> Self { - Self { - _config: config, - query: None, - } - } - - /// 设置查询词 - pub fn query(mut self, query: impl Into) -> Self { - self.query = Some(query.into()); - self - } - - fn unsupported(operation: &str) -> openlark_core::error::CoreError { - business_error(format!( - "{operation}: openlark-analytics 尚未接入该 search runtime API,请改用已实现的 doc_wiki/schema/app/message 路径" - )) - } - - /// 执行请求。 - pub async fn execute(self) -> SDKResult { - self.execute_with_options(RequestOption::default()).await - } - - /// 执行请求并传入请求选项。 - pub async fn execute_with_options( - self, - _option: RequestOption, - ) -> SDKResult { - Err(Self::unsupported("query.suggest")) - } -} - -#[cfg(test)] -#[allow(unused_imports)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_query_stub_returns_explicit_error() { - let config = Arc::new(AnalyticsConfig::default()); - let err = QueryApi::new(config) - .search() - .search_term("项目文档") - .execute() - .await - .expect_err("query search should now fail explicitly"); - assert!(err.to_string().contains("尚未接入")); - } -} diff --git a/crates/openlark-analytics/src/search/search/v2/user.rs b/crates/openlark-analytics/src/search/search/v2/user.rs deleted file mode 100644 index 4e1007c25..000000000 --- a/crates/openlark-analytics/src/search/search/v2/user.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! 用户搜索 API -//! -//! 当前仍是 runtime stub。 -//! -//! 该入口没有对应的已接线服务端实现,因此现在会显式返回未接线错误, -//! 而不再伪装成成功返回占位 JSON。 - -use crate::AnalyticsConfig; -use openlark_core::{SDKResult, error::business_error, req_option::RequestOption}; -use std::sync::Arc; - -/// 用户搜索 API -#[derive(Debug, Clone)] -pub struct UserSearchApi { - config: Arc, -} - -impl UserSearchApi { - /// 创建新的用户搜索 facade。 - pub fn new(config: Arc) -> Self { - Self { config } - } - - /// 搜索用户 - pub fn search(&self) -> SearchUserRequest { - SearchUserRequest::new(self.config.clone()) - } -} - -/// 搜索用户请求 -pub struct SearchUserRequest { - // reserved:待装访问器/execute(见 #274,不完整脚手架) - _config: Arc, - query: Option, - page_size: Option, -} - -impl SearchUserRequest { - fn new(config: Arc) -> Self { - Self { - _config: config, - query: None, - page_size: None, - } - } - - /// 设置查询词 - pub fn query(mut self, query: impl Into) -> Self { - self.query = Some(query.into()); - self - } - - /// 设置页面大小 - pub fn page_size(mut self, size: u32) -> Self { - self.page_size = Some(size); - self - } - - fn unsupported() -> openlark_core::error::CoreError { - business_error( - "user.search: openlark-analytics 尚未接入用户搜索 runtime API,请等待后续真实端点支持", - ) - } - - /// 执行请求。 - pub async fn execute(self) -> SDKResult { - self.execute_with_options(RequestOption::default()).await - } - - /// 执行请求并传入请求选项。 - pub async fn execute_with_options( - self, - _option: RequestOption, - ) -> SDKResult { - Err(Self::unsupported()) - } -} - -#[cfg(test)] -#[allow(unused_imports)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_user_search_stub_returns_explicit_error() { - let config = Arc::new(AnalyticsConfig::default()); - let err = UserSearchApi::new(config) - .search() - .query("zool") - .execute() - .await - .expect_err("user search should now fail explicitly"); - assert!(err.to_string().contains("尚未接入")); - } -} diff --git a/crates/openlark-analytics/src/service.rs b/crates/openlark-analytics/src/service.rs index 7499f18d6..26d34dee2 100644 --- a/crates/openlark-analytics/src/service.rs +++ b/crates/openlark-analytics/src/service.rs @@ -3,7 +3,6 @@ //! 提供数据分析相关的服务入口 use crate::AnalyticsConfig; -use openlark_core::SDKResult; use std::sync::Arc; /// 数据分析服务 @@ -16,19 +15,14 @@ pub struct AnalyticsService { } impl AnalyticsService { - /// 创建新的数据分析服务实例 + /// 创建新的数据分析服务实例。 /// - /// # 参数 - /// - /// * `config` - 数据分析服务配置 - /// - /// # 返回 - /// - /// 返回数据分析服务实例或错误 - pub fn new(config: AnalyticsConfig) -> SDKResult { - Ok(Self { + /// 构造不会失败,故返回 `Self`(非 `SDKResult`)——#350 P9 接口形状撒谎修正, + /// 与 `PlatformService::new` / `UserService::new` 一致。 + pub fn new(config: AnalyticsConfig) -> Self { + Self { config: Arc::new(config), - }) + } } /// 获取客户端配置 @@ -50,6 +44,6 @@ mod tests { .build(); let service = AnalyticsService::new(config); - assert!(service.is_ok()); + assert_eq!(service.config().app_id(), "test_app_id"); } } diff --git a/crates/openlark-client/src/client.rs b/crates/openlark-client/src/client.rs index a5eb4597e..622a87c2f 100644 --- a/crates/openlark-client/src/client.rs +++ b/crates/openlark-client/src/client.rs @@ -159,7 +159,7 @@ declare_client! { ty: crate::AnalyticsClient, doc: "Analytics meta 调用链入口:client.analytics.report... ...", init: |_core_config, _base_core_config| { - crate::AnalyticsClient::new(_core_config.clone())? + crate::AnalyticsClient::new(_core_config.clone()) }, }, { diff --git a/crates/openlark-workflow/Cargo.toml b/crates/openlark-workflow/Cargo.toml index d36ba28cb..c38bfc6de 100644 --- a/crates/openlark-workflow/Cargo.toml +++ b/crates/openlark-workflow/Cargo.toml @@ -23,6 +23,7 @@ tracing = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["full"] } insta = { workspace = true } +wiremock = { workspace = true } [features] default = ["v1", "v2", "async", "board"] diff --git a/crates/openlark-workflow/src/service.rs b/crates/openlark-workflow/src/service.rs index 4e0550003..ee10578c0 100644 --- a/crates/openlark-workflow/src/service.rs +++ b/crates/openlark-workflow/src/service.rs @@ -282,13 +282,6 @@ impl ApprovalTaskAction { /// 审批任务条目类型别名。 pub type ApprovalTaskItem = crate::approval::approval::v4::task::query::TaskItemV4; -/// 审批任务动作结果 helper。 -#[derive(Debug, Clone, PartialEq)] -pub struct ApprovalTaskActionResult { - /// 操作是否成功。 - pub success: bool, -} - /// WorkflowService:工作流服务的统一入口 /// /// 提供对任务、审批、看板 API 的访问能力 @@ -464,10 +457,10 @@ impl WorkflowService { } /// 同意审批任务 helper。 - pub async fn approve_task( - &self, - action: ApprovalTaskAction, - ) -> SDKResult { + /// + /// 成功/失败由 `SDKResult` 表达:飞书 approval v4 同意接口响应 data 为空, + /// 不再伪造恒为 `true` 的 `success` 字段(#350 P9 接口形状撒谎修正)。 + pub async fn approve_task(&self, action: ApprovalTaskAction) -> SDKResult<()> { let mut request = crate::approval::approval::v4::task::approve::ApproveTaskRequestV4::new( self.config.clone(), ) @@ -484,16 +477,14 @@ impl WorkflowService { if let Some(form) = action.form { request = request.form(form); } - let response = request.execute().await?; - let _ = response; - Ok(ApprovalTaskActionResult { success: true }) + request.execute().await?; + Ok(()) } /// 拒绝审批任务 helper。 - pub async fn reject_task( - &self, - action: ApprovalTaskAction, - ) -> SDKResult { + /// + /// 成功/失败由 `SDKResult` 表达;响应 data 为空时不伪造 `success: true`。 + pub async fn reject_task(&self, action: ApprovalTaskAction) -> SDKResult<()> { let mut request = crate::approval::approval::v4::task::reject::RejectTaskRequestV4::new( self.config.clone(), ) @@ -510,16 +501,14 @@ impl WorkflowService { if let Some(form) = action.form { request = request.form(form); } - let response = request.execute().await?; - let _ = response; - Ok(ApprovalTaskActionResult { success: true }) + request.execute().await?; + Ok(()) } /// 重新提交审批任务 helper。 - pub async fn resubmit_task( - &self, - action: ApprovalTaskAction, - ) -> SDKResult { + /// + /// 成功/失败由 `SDKResult` 表达;响应 data 为空时不伪造 `success: true`。 + pub async fn resubmit_task(&self, action: ApprovalTaskAction) -> SDKResult<()> { let mut request = crate::approval::approval::v4::task::resubmit::ResubmitTaskRequestV4::new( self.config.clone(), @@ -537,9 +526,8 @@ impl WorkflowService { if let Some(form) = action.form { request = request.form(form); } - let response = request.execute().await?; - let _ = response; - Ok(ApprovalTaskActionResult { success: true }) + request.execute().await?; + Ok(()) } } @@ -618,4 +606,100 @@ mod tests { assert_eq!(action.comment.as_deref(), Some("已确认")); assert_eq!(action.form.as_deref(), Some("[{}]")); } + + /// #350:approve/reject/resubmit 成功时返回 `Ok(())`,不再伪造恒真 `success`。 + #[tokio::test] + async fn test_approve_reject_resubmit_helpers_return_unit_on_success() { + use wiremock::matchers::{method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + let server = MockServer::start().await; + for p in [ + "/open-apis/approval/v4/tasks/approve", + "/open-apis/approval/v4/tasks/reject", + "/open-apis/approval/v4/tasks/resubmit", + ] { + Mock::given(method("POST")) + .and(path(p)) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "code": 0, + "msg": "success", + "data": {} + }))) + .mount(&server) + .await; + } + + let service = WorkflowService::new( + Config::builder() + .app_id("ci_app_id") + .app_secret("ci_app_secret") + .base_url(server.uri()) + .enable_token_cache(false) + .build(), + ); + + let action = + ApprovalTaskAction::new("approval_code", "instance_code", "ou_xxx", "task_123") + .user_id_type("open_id") + .comment("ok") + .form("[]"); + + service + .approve_task(action.clone()) + .await + .expect("approve_task 应在飞书成功响应时返回 Ok(())"); + service + .reject_task(action.clone()) + .await + .expect("reject_task 应在飞书成功响应时返回 Ok(())"); + service + .resubmit_task(action) + .await + .expect("resubmit_task 应在飞书成功响应时返回 Ok(())"); + } + + /// #350:底层 API 失败时 helper 传播 Err,而非恒真 success。 + #[tokio::test] + async fn test_approve_task_helper_propagates_api_error() { + use wiremock::matchers::{method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/open-apis/approval/v4/tasks/approve")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "code": 99991663, + "msg": "invalid approval task" + }))) + .mount(&server) + .await; + + let service = WorkflowService::new( + Config::builder() + .app_id("ci_app_id") + .app_secret("ci_app_secret") + .base_url(server.uri()) + .enable_token_cache(false) + .build(), + ); + + let err = service + .approve_task(ApprovalTaskAction::new( + "approval_code", + "instance_code", + "ou_xxx", + "task_123", + )) + .await + .expect_err("API 业务错误应传播为 Err"); + let msg = err.to_string(); + assert!( + msg.contains("99991663") + || msg.contains("invalid approval task") + || msg.contains("验证错误") + || msg.contains("业务"), + "unexpected error (should be API/business failure, not silent Ok): {err}" + ); + } } diff --git a/docs/RUNTIME_API_STUB_AUDIT.md b/docs/RUNTIME_API_STUB_AUDIT.md index 90dab60ee..1ed2b2e8e 100644 --- a/docs/RUNTIME_API_STUB_AUDIT.md +++ b/docs/RUNTIME_API_STUB_AUDIT.md @@ -6,39 +6,41 @@ ## 范围 -本轮覆盖以下 11 个仍在追踪的 source-level TODO API stubs(原 18 个中 `openlark-user` 的 7 个 settings/preferences stub 已由 #311 删除:stub 链移除 + 门面补 `personal_settings()` accessor 指向真实 system_status): +本轮覆盖以下仍在追踪的 source-level TODO API stubs(原 18 个中多项已删除): -| Crate | File | Stub methods | Count | -| --- | --- | --- | ---: | -| `openlark-analytics` | `crates/openlark-analytics/src/search/search/v2/query.rs` | `SearchRequest::execute`, `SuggestRequest::execute` | 2 | -| `openlark-analytics` | `crates/openlark-analytics/src/search/search/v2/user.rs` | `SearchUserRequest::execute` | 1 | -| `openlark-platform` | `crates/openlark-platform/src/admin/admin/v1/settings.rs` | `GetSettingRequest::execute`, `UpdateSettingRequest::execute`, `ListSettingsRequest::execute` | 3 | -| `openlark-platform` | `crates/openlark-platform/src/admin/admin/v1/users.rs` | `ListAdminUsersRequest::execute`, `DisableUserRequest::execute`, `EnableUserRequest::execute` | 3 | -| `openlark-platform` | `crates/openlark-platform/src/admin/admin/v1/audit.rs` | `QueryAuditLogsRequest::execute`, `GetAuditLogRequest::execute` | 2 | +| Crate | File | Stub methods | Count | 状态 | +| --- | --- | --- | ---: | --- | +| `openlark-analytics` | `search/search/v2/query.rs`、`user.rs` | `SearchRequest`/`SuggestRequest`/`SearchUserRequest::execute` | 3 | ✅ #350 删除:无已验证飞书端点,恒 `Err` + setter 死值是接口撒谎 | +| `openlark-user` | settings/preferences 链 | 7 个 `*Request::execute` | 7 | ✅ #311 删除 | +| `openlark-platform` | `admin/v1/settings.rs` | `Get/Update/ListSettingsRequest::execute` | 3 | 待 #110 | +| `openlark-platform` | `admin/v1/users.rs` | `List/Disable/Enable*Request::execute` | 3 | 待 #110 | +| `openlark-platform` | `admin/v1/audit.rs` | `Query/GetAuditLog*Request::execute` | 2 | 待 #110 | ## 风险分组 -### 1. Analytics 搜索 stubs +### 1. Analytics 搜索 stubs(✅ 已由 #350 解决) -影响: - -- 看起来像可用的搜索 API,但默认只返回占位结构 -- 容易让调用方误以为已经接入了真实搜索服务 +**状态**:已移除。`query.rs` / `user.rs` 的 `QueryApi`/`UserSearchApi`/`SearchRequest`/`SuggestRequest`/`SearchUserRequest` +曾以 builder + `execute()` 形状暴露,但无已验证的飞书 runtime 端点(#108/#2fab71234 约束:不发明未验证端点), +`execute()` 恒 `Err`,setter 存值从不读取——#350 P9「接口形状撒谎」。与 #308 删除 `Search`/`SearchV2` 门面死链一致: +未接线 surface 直接删,不保留恒失败脚手架。调用方继续用已实现的 `doc_wiki`/`schema`/`app`/`message`/`data_source`。 跟踪 issue: -- `#108` Implement analytics search runtime TODO stubs +- `#108` Implement analytics search runtime TODO stubs(由 #350 关闭方向:stub 删除,不再待实现) +- `#350` P9 接口形状撒谎(analytics 子项) ### 2. User 设置 / 偏好 stubs(✅ 已由 #311 解决) **状态**:已移除。`openlark-user` 的 settings/preferences stub 链(7 个 `*Request::execute`)连同 `SettingsService` / `PreferencesService` 门面与 `settings` / `preferences` / `v1` 等 feature 一并删除 -(v0.18 breaking)。门面 `UserService` 改补 `personal_settings()` accessor,指向真实 system_status -资源(7 个 live 请求构建器)。原影响(占位返回误导集成方)随之消除。 +(v0.18 breaking)。门面 `UserService` 改补 `system_status()` accessor,指向真实 system_status +资源(7 个 live 请求构建器)。`UserService::new` 误导 `SDKResult` 签名由 #360/#350 改为 `Self`。 跟踪 issue: - `#109` Implement user settings and preferences runtime TODO stubs(由 #311 关闭:stub 删除,不再待实现) +- `#350` P9 user 子项(`UserService::new` → #360) ### 3. Platform admin stubs @@ -46,6 +48,7 @@ - 平台后台管理接口(settings / users / audit)名称清晰、语义明确 - 当前占位实现会造成“存在接口、但并未真正接线”的错觉 +- 注:#350 platform 子项已修 `PlatformService::new` 误导 `SDKResult` 签名(#373);admin 恒 `Err` stub 仍归 #110 跟踪 issue: @@ -53,9 +56,8 @@ ## 当前结论 -- 这些 runtime TODO stubs **不应继续处于无主状态** -- 本轮不强行在一个 issue 里实现全部 18 处 -- 已按业务域拆成 3 个 follow-up issues,便于分批收敛 +- analytics / user runtime TODO stubs 已删除;剩余主要为 platform admin 8 处(#110) +- 未接线 surface 优先删除或改签名,不保留恒失败/`success: true` 的撒谎脚手架 ## 与 TODO 总审计的关系 diff --git a/docs/communication-workflow-helper-boundaries.md b/docs/communication-workflow-helper-boundaries.md index 28a71ebdb..c67c2bbc4 100644 --- a/docs/communication-workflow-helper-boundaries.md +++ b/docs/communication-workflow-helper-boundaries.md @@ -103,9 +103,7 @@ - `ApprovalTaskQuery` - `ApprovalTaskAction` - `query_approval_tasks` - - `approve_task` - - `reject_task` - - `resubmit_task` + - `approve_task` / `reject_task` / `resubmit_task`(返回 `SDKResult<()>`;成功/失败只由 `Result` 表达,不伪造 `success: bool`,见 #350) ### 4.3 Workflow helper 设计规则 diff --git a/examples/01_getting_started/communication_workflows.rs b/examples/01_getting_started/communication_workflows.rs index f1c0e58ce..5142a1a5d 100644 --- a/examples/01_getting_started/communication_workflows.rs +++ b/examples/01_getting_started/communication_workflows.rs @@ -134,7 +134,7 @@ async fn workflow_execution_flow( println!("审批任务数: {}", tasks.len()); if let Some(task) = tasks.first() { - let approved = client + client .workflow .approve_task( ApprovalTaskAction::new( @@ -147,7 +147,7 @@ async fn workflow_execution_flow( .comment("workflow 示例审批通过"), ) .await?; - println!("审批处理结果: {}", approved.success); + println!("审批处理成功"); } else { println!("未查询到待处理审批任务,跳过审批动作"); } diff --git a/examples/workflow_api_example.rs b/examples/workflow_api_example.rs index 9a10ec8c6..0810d410f 100644 --- a/examples/workflow_api_example.rs +++ b/examples/workflow_api_example.rs @@ -50,7 +50,7 @@ async fn main() -> Result<(), Box> { println!("待审批任务数量: {}", approval_tasks.len()); if let Some(task) = approval_tasks.first() { - let result = workflow_service + workflow_service .approve_task( ApprovalTaskAction::new( task.approval_code.clone(), @@ -62,7 +62,7 @@ async fn main() -> Result<(), Box> { .comment("自动化示例通过"), ) .await?; - println!("审批处理结果: {}", result.success); + println!("审批处理成功"); } Ok(()) diff --git a/openspec/specs/no-deprecated-compat-accessors/spec.md b/openspec/specs/no-deprecated-compat-accessors/spec.md index 2d289633b..6cf32ecf2 100644 --- a/openspec/specs/no-deprecated-compat-accessors/spec.md +++ b/openspec/specs/no-deprecated-compat-accessors/spec.md @@ -21,9 +21,9 @@ openlark-analytics 的 `SearchV2` SHALL 不再提供 `query()`/`user()` deprecat - **WHEN** 在 `crates/openlark-analytics/src/search/search/v2.rs` 中 grep `pub fn query|pub fn user` - **THEN** 命中数为 0(2 个 deprecated 存根访问器移除) -#### Scenario: QueryApi/UserSearchApi 类型保留 -- **WHEN** 移除存根访问器后构建 openlark-analytics -- **THEN** `v2/query.rs`、`v2/user.rs` 模块与 `QueryApi`/`UserSearchApi` 类型仍 `pub` 可用(经完整路径),仅便捷存根方法被移除 +#### Scenario: QueryApi/UserSearchApi 恒失败 stub 已删除(#350 收口) +- **WHEN** 在 `crates/openlark-analytics/src/search/search/v2` 下查找 `query.rs`/`user.rs` 或 `QueryApi`/`UserSearchApi` +- **THEN** 模块与类型均不存在(#350 P9:无已验证端点的恒 `Err` + setter 死值 stub 删除;改用 `doc_wiki`/`schema`/`app`/`message`/`data_source`) ### Requirement: 移除不破坏构建与测试 本次移除 SHALL 不导致 default/full/no-default 任一 feature 组合的 clippy 或测试失败。 From c212976b5dbd3bf44d9dd7946719b467dd41713a Mon Sep 17 00:00:00 2001 From: ZoOL Date: Fri, 10 Jul 2026 13:55:10 +0800 Subject: [PATCH 2/3] fix(ci): sync msrv lockfile for openlark-workflow wiremock dev-dep MSRV job copies .github/msrv/Cargo.lock and runs cargo check --locked; workflow gained wiremock for approve helper e2e tests (#350). --- .github/msrv/Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/msrv/Cargo.lock b/.github/msrv/Cargo.lock index d39708c1b..ccbff632b 100644 --- a/.github/msrv/Cargo.lock +++ b/.github/msrv/Cargo.lock @@ -1894,6 +1894,7 @@ dependencies = [ "serde_json", "tokio", "tracing", + "wiremock", ] [[package]] From d26625cdc27d6fcc3556776d5e8724d7232d934f Mon Sep 17 00:00:00 2001 From: ZoOL Date: Fri, 10 Jul 2026 14:01:58 +0800 Subject: [PATCH 3/3] test(workflow): tighten #350 helper e2e assertions and migration docs - Assert approve/reject/resubmit hit the three Feishu paths via wiremock - Propagate-error test uses neutral business code 99991400 and matches real leaf behavior (Validation on non-zero code without data) - Clarify analytics stub migration: never wired, user search still absent - Fix stale #308 alternative paths that pointed at deleted query/user --- CHANGELOG.md | 9 ++-- crates/openlark-workflow/src/service.rs | 48 +++++++++++++++---- .../no-deprecated-compat-accessors/spec.md | 4 +- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01fd6c8e0..3827ad1d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (`QueryApi`/`UserSearchApi`/`SearchRequest`/`SuggestRequest`/`SearchUserRequest`)。 无已验证飞书端点(与 #2fab71234 / #108 约束一致:不发明未验证端点);setter 死值 + `execute()` 恒失败是接口撒谎。与 #308 删除 `Search`/`SearchV2` 门面死链同向收口。 - **迁移**:改用已实现的 `doc_wiki`/`schema`/`app`/`message`/`data_source` leaf builder。 + **迁移**:这些 stub 从未接线,不是其它 leaf 的别名——请改用已实现的 search leaf + (`doc_wiki`/`schema`/`app`/`message`/`data_source`)。**用户搜索仍无 surface**。 2. `AnalyticsService::new` 误导签名 `SDKResult` 但函数体永远 `Ok(...)` → 改为 `Self` (同 platform #373 / user #360)。**迁移**:`AnalyticsService::new(config)?` / client facade 去 `?`。 @@ -208,9 +209,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **analytics Search / SearchV2 / search() 标 `#[deprecated]`**(#308):三层 `Arc` 导航死胡同(Search → SearchV2 无真实 API 落地)标记 deprecated,note 指明替代路径 - (v2 子模块的 `XxxRequest::new`,如 `query::SearchRequest` / `user::SearchUserRequest`)。 - 配合 v0.18 deprecated 清理节奏,下个 breaking 窗口删除。**非 breaking**:仅 deprecation - warning,旧调用仍可编译。 + (v2 已实现 leaf:`doc_wiki` / `schema` / `app` / `message` / `data_source` 的 `XxxRequest::new`)。 + 配合 v0.18 deprecated 清理节奏,下个 breaking 窗口删除(#350 已删恒 `Err` 的 `query`/`user` + stub)。**非 breaking**:仅 deprecation warning,旧调用仍可编译。 ### Breaking Changes diff --git a/crates/openlark-workflow/src/service.rs b/crates/openlark-workflow/src/service.rs index ee10578c0..c0997a99d 100644 --- a/crates/openlark-workflow/src/service.rs +++ b/crates/openlark-workflow/src/service.rs @@ -614,11 +614,12 @@ mod tests { use wiremock::{Mock, MockServer, ResponseTemplate}; let server = MockServer::start().await; - for p in [ + let paths = [ "/open-apis/approval/v4/tasks/approve", "/open-apis/approval/v4/tasks/reject", "/open-apis/approval/v4/tasks/resubmit", - ] { + ]; + for p in paths { Mock::given(method("POST")) .and(path(p)) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ @@ -657,11 +658,30 @@ mod tests { .resubmit_task(action) .await .expect("resubmit_task 应在飞书成功响应时返回 Ok(())"); + + let received = server.received_requests().await.unwrap_or_default(); + assert_eq!( + received.len(), + 3, + "三个 helper 应各打一次飞书 approval v4 端点" + ); + let hit: Vec<_> = received.iter().map(|r| r.url.path().to_string()).collect(); + for p in paths { + assert!( + hit.iter().any(|h| h == p), + "missing request to {p}; got {hit:?}" + ); + } } - /// #350:底层 API 失败时 helper 传播 Err,而非恒真 success。 + /// #350:底层失败时 helper 传播 Err,而非恒真 success。 + /// + /// `ApproveTaskRequestV4` 对飞书 `code != 0` 且无 `data` 的响应走 + /// `missing_response_data`(Validation),而不是把 `msg` 映射成 `CoreError::Api`。 + /// 本测试锁定 helper 契约:`Err` 必须向上抛出,不能伪装 `Ok(())`。 #[tokio::test] async fn test_approve_task_helper_propagates_api_error() { + use openlark_core::error::CoreError; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -669,7 +689,7 @@ mod tests { Mock::given(method("POST")) .and(path("/open-apis/approval/v4/tasks/approve")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "code": 99991663, + "code": 99991400, "msg": "invalid approval task" }))) .mount(&server) @@ -692,14 +712,22 @@ mod tests { "task_123", )) .await - .expect_err("API 业务错误应传播为 Err"); + .expect_err("飞书失败响应应传播为 Err,不得伪装 Ok(())"); + assert!( + matches!(err, CoreError::Validation { .. } | CoreError::Api(_)), + "expected Validation (missing data on non-zero code) or Api, got {err:?}" + ); let msg = err.to_string(); assert!( - msg.contains("99991663") - || msg.contains("invalid approval task") - || msg.contains("验证错误") - || msg.contains("业务"), - "unexpected error (should be API/business failure, not silent Ok): {err}" + msg.contains("服务器没有返回有效的数据") || msg.contains("invalid approval task"), + "error should surface leaf validation or Feishu msg, got: {err}" + ); + + let received = server.received_requests().await.unwrap_or_default(); + assert_eq!(received.len(), 1); + assert_eq!( + received[0].url.path(), + "/open-apis/approval/v4/tasks/approve" ); } } diff --git a/openspec/specs/no-deprecated-compat-accessors/spec.md b/openspec/specs/no-deprecated-compat-accessors/spec.md index 6cf32ecf2..c3489f9a1 100644 --- a/openspec/specs/no-deprecated-compat-accessors/spec.md +++ b/openspec/specs/no-deprecated-compat-accessors/spec.md @@ -23,7 +23,9 @@ openlark-analytics 的 `SearchV2` SHALL 不再提供 `query()`/`user()` deprecat #### Scenario: QueryApi/UserSearchApi 恒失败 stub 已删除(#350 收口) - **WHEN** 在 `crates/openlark-analytics/src/search/search/v2` 下查找 `query.rs`/`user.rs` 或 `QueryApi`/`UserSearchApi` -- **THEN** 模块与类型均不存在(#350 P9:无已验证端点的恒 `Err` + setter 死值 stub 删除;改用 `doc_wiki`/`schema`/`app`/`message`/`data_source`) +- **THEN** 模块与类型均不存在(#350 P9:无已验证端点的恒 `Err` + setter 死值 stub 删除; + 调用方使用已实现 search leaf `doc_wiki`/`schema`/`app`/`message`/`data_source`; + 用户搜索仍无 surface,勿把已删 stub 当作这些 leaf 的别名) ### Requirement: 移除不破坏构建与测试 本次移除 SHALL 不导致 default/full/no-default 任一 feature 组合的 clippy 或测试失败。