@steedos-labs/plugin-workflow 3.0.106 → 3.0.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  waitForThing(window, 'antd').then(function(){
2
- var v = '3.0.106';
2
+ var v = '3.0.107';
3
3
  loadJs('/amis-renderer/amis-renderer.js?v=' + v);
4
4
  loadCss('/amis-renderer/amis-renderer.css?v=' + v)
5
5
  })
@@ -2044,6 +2044,28 @@ UUFlowManager.handleSkipProcessed = async function (instance_id, flow, maxDepth
2044
2044
  return;
2045
2045
  }
2046
2046
 
2047
+ // 滑步派发由服务端直接解析处理人,绕过了 nextStepUsers router 的
2048
+ // user_accepted 过滤;此处补齐同口径过滤,已停用账号不派发待办。
2049
+ // 若过滤后无人,走下方既有的「无处理人 → 中止滑步」兜底。
2050
+ const spaceUsersCollection = await getCollection('space_users');
2051
+ if (next_step_users && next_step_users.length > 0) {
2052
+ const nextStepSpaceUsers = await spaceUsersCollection.find(
2053
+ { space: space_id, user: { $in: next_step_users } },
2054
+ { projection: { user: 1, user_accepted: 1 } }
2055
+ ).toArray();
2056
+ const acceptedUserIds = [];
2057
+ for (const su of nextStepSpaceUsers) {
2058
+ if (su.user_accepted) {
2059
+ acceptedUserIds.push(su.user);
2060
+ }
2061
+ }
2062
+ const deactivatedUserIds = next_step_users.filter(u => !acceptedUserIds.includes(u));
2063
+ if (deactivatedUserIds.length > 0) {
2064
+ console.log(`[workflow/engine] [handleSkipProcessed] Filtered ${deactivatedUserIds.length} handler(s) without accepted space_users record (user_accepted=false or missing) for next step "${next_step.name}": ${deactivatedUserIds.join(', ')}`);
2065
+ next_step_users = next_step_users.filter(u => acceptedUserIds.includes(u));
2066
+ }
2067
+ }
2068
+
2047
2069
  if (!next_step_users || next_step_users.length === 0) {
2048
2070
  console.warn('[workflow/engine] [handleSkipProcessed] No handlers found for next step after skip');
2049
2071
  return;
@@ -2068,7 +2090,6 @@ UUFlowManager.handleSkipProcessed = async function (instance_id, flow, maxDepth
2068
2090
  };
2069
2091
 
2070
2092
  const usersCollection = await getCollection('users');
2071
- const spaceUsersCollection = await getCollection('space_users');
2072
2093
  const updatedValues = await UUFlowManager.getUpdatedValues(instance);
2073
2094
 
2074
2095
  const traceUserIds = [];
@@ -0,0 +1,243 @@
1
+ /**
2
+ * 回归测试:滑步(handleSkipProcessed)派发下一步处理人时,
3
+ * 必须过滤 user_accepted=false 的已停用账号。
4
+ *
5
+ * 背景(DEFECT D-1(d),集团IT需求及立项审批申请 9d861de0f5a17e422b03bb0e):
6
+ * - 正常派发走 nextStepUsers router,router 里有 `if (su.user_accepted)` 过滤;
7
+ * - 滑步派发由服务端自行调用 HandlersManager.getHandlers 解析处理人,
8
+ * 完全绕开 router 过滤 → 已停用账号(user_accepted=false)被派成真实待办。
9
+ * 实测:specifyOrg 8 人部门中 3 个停用账号被派发(正常路径 5 人 / 滑步 8 人)。
10
+ *
11
+ * 期望(修复后):
12
+ * - 滑步派发前按 space + user 查 space_users,只保留 user_accepted 为真的处理人;
13
+ * - 过滤后无人 → 复用既有「无处理人 → 中止滑步」兜底,实例停留在当前步骤;
14
+ * - 非 counterSign 步骤取"过滤后"的第一个处理人(停用账号不能占首位)。
15
+ *
16
+ * 用法:
17
+ * node main/default/test/test_handleSkipProcessed_userAccepted.js
18
+ * 或 npm run test:skip-user-accepted
19
+ */
20
+
21
+ const assert = require('assert');
22
+
23
+ // uuflow_manager 依赖运行时全局 `_`(lodash,由 Steedos server 注入),
24
+ // 独立跑测试时手动补上。
25
+ global._ = global._ || require('lodash');
26
+
27
+ // 滑步开关不受本机 shell 环境影响
28
+ delete process.env.WORKFLOW_SKIP_PROCESSED_ENABLED;
29
+ delete process.env.STEEDOS_WORKFLOW_SKIP_CONSECUTIVE_ONLY;
30
+
31
+ // ---- 注入假 getCollection(姿势同 test_handleSkipProcessed_handlerError.js)----
32
+ const state = {
33
+ updateCalls: [],
34
+ pushedTraces: [],
35
+ spaceUsersDocs: [], // 每个用例自行设置
36
+ };
37
+
38
+ function resetState() {
39
+ state.updateCalls = [];
40
+ state.pushedTraces = [];
41
+ state.spaceUsersDocs = [];
42
+ }
43
+
44
+ let idSeq = 0;
45
+ const fakeCollections = {
46
+ instances: {
47
+ updateOne: async function (query, update) {
48
+ state.updateCalls.push(update);
49
+ if (update.$push && update.$push.traces) {
50
+ state.pushedTraces.push(update.$push.traces);
51
+ }
52
+ return { modifiedCount: 1 };
53
+ },
54
+ findOne: async function () { return null; },
55
+ find: function () { return { toArray: async () => [] }; },
56
+ },
57
+ space_users: {
58
+ find: function (query) {
59
+ // 红线:查 space_users 必须带 space 过滤
60
+ assert.strictEqual(query.space, 'space1', 'space_users 查询必须带 space 过滤');
61
+ const ids = (query.user && query.user.$in) || [];
62
+ return { toArray: async () => state.spaceUsersDocs.filter(d => ids.includes(d.user)) };
63
+ },
64
+ findOne: async function () { return null; },
65
+ },
66
+ users: {
67
+ findOne: async function (query) { return { _id: query._id, name: 'name_' + query._id }; },
68
+ },
69
+ __default: {
70
+ updateOne: async function () { return { modifiedCount: 1 }; },
71
+ findOne: async function () { return null; },
72
+ find: function () { return { toArray: async () => [] }; },
73
+ },
74
+ };
75
+
76
+ const collectionPath = require.resolve('../utils/collection.js');
77
+ require.cache[collectionPath] = {
78
+ id: collectionPath,
79
+ filename: collectionPath,
80
+ loaded: true,
81
+ exports: {
82
+ getCollection: async (name) => fakeCollections[name] || fakeCollections.__default,
83
+ _makeNewID: () => 'fake_id_' + (++idSeq),
84
+ },
85
+ };
86
+
87
+ // instance_tasks 写库与本测试无关,桩成 noop
88
+ const tasksPath = require.resolve('../manager/instance_tasks_manager.js');
89
+ require.cache[tasksPath] = {
90
+ id: tasksPath,
91
+ filename: tasksPath,
92
+ loaded: true,
93
+ exports: {
94
+ insert_instance_tasks: async () => {},
95
+ insert_many_instance_tasks: async () => {},
96
+ update_instance_tasks: async () => {},
97
+ update_many_instance_tasks: async () => {},
98
+ remove_many_instance_tasks: async () => {},
99
+ },
100
+ };
101
+
102
+ const UUFlowManager = require('../manager/uuflow_manager.js');
103
+ const HandlersManager = require('../manager/handlers_manager.js');
104
+
105
+ let passed = 0;
106
+ let failed = 0;
107
+
108
+ async function run(name, fn) {
109
+ try {
110
+ await fn();
111
+ console.log(` ✓ ${name}`);
112
+ passed++;
113
+ } catch (e) {
114
+ console.log(` ✗ ${name}`);
115
+ console.log(` ${e.message}`);
116
+ failed++;
117
+ }
118
+ }
119
+
120
+ const APPLICANT = 'user_applicant';
121
+
122
+ function buildFakeInstance() {
123
+ return {
124
+ _id: 'ins1',
125
+ state: 'pending',
126
+ space: 'space1',
127
+ applicant: APPLICANT,
128
+ // 与 next_step.name 相同 → 绕开 getObject('instances') 的台账同步写入
129
+ current_step_name: 'Step2',
130
+ processed_user_ids: [APPLICANT],
131
+ traces: [
132
+ {
133
+ _id: 'trace1',
134
+ step: 'step1',
135
+ is_finished: false,
136
+ previous_trace_ids: [],
137
+ approves: [
138
+ { _id: 'app1', user: APPLICANT, handler: APPLICANT },
139
+ ],
140
+ },
141
+ ],
142
+ };
143
+ }
144
+
145
+ // 每个用例:step1(处理人=申请人,触发整步滑步)→ step2(滑步派发目标)
146
+ function setupCase(nextStepType, handlers, spaceUsersDocs) {
147
+ resetState();
148
+ state.spaceUsersDocs = spaceUsersDocs;
149
+
150
+ const fakeInstance = buildFakeInstance();
151
+ let getInstanceCalls = 0;
152
+ UUFlowManager.getInstance = async function () {
153
+ getInstanceCalls++;
154
+ if (getInstanceCalls === 1) return fakeInstance;
155
+ // 递归第二次进入时返回"已结束"的 trace,终止递归
156
+ return Object.assign({}, fakeInstance, { traces: [{ _id: 'trace2', is_finished: true }] });
157
+ };
158
+ UUFlowManager.getStep = function (instance, flow, stepId) {
159
+ if (stepId === 'step1') {
160
+ return {
161
+ _id: 'step1', name: 'Step1', step_type: 'sign',
162
+ skip_enabled: true,
163
+ lines: [{ state: 'approved', to_step: 'step2' }],
164
+ };
165
+ }
166
+ if (stepId === 'step2') {
167
+ return { _id: 'step2', name: 'Step2', step_type: nextStepType };
168
+ }
169
+ throw new Error('unexpected step ' + stepId);
170
+ };
171
+ UUFlowManager.getAgent = async function () { return null; };
172
+ UUFlowManager.getDueDate = async function () { return null; };
173
+ UUFlowManager.getUpdatedValues = async function () { return {}; };
174
+ UUFlowManager.setRemindInfo = function () {};
175
+ HandlersManager.getHandlers = async function () { return handlers.slice(); };
176
+ }
177
+
178
+ (async () => {
179
+ console.log('UUFlowManager.handleSkipProcessed 滑步派发过滤 user_accepted=false');
180
+
181
+ await run('counterSign:8 人含停用 → 只给在职账号建 approve,停用账号不进 inbox', async () => {
182
+ setupCase('counterSign', ['uA', 'uB', 'uC'], [
183
+ { user: 'uA', user_accepted: true },
184
+ { user: 'uB', user_accepted: false }, // 已停用
185
+ { user: 'uC', user_accepted: true },
186
+ ]);
187
+
188
+ await UUFlowManager.handleSkipProcessed('ins1', { _id: 'flow1' });
189
+
190
+ assert.strictEqual(state.pushedTraces.length, 1, '应派发出一条新 trace');
191
+ const approveUsers = state.pushedTraces[0].approves.map(a => a.user);
192
+ assert.deepStrictEqual(approveUsers, ['uA', 'uC'],
193
+ '停用账号 uB 不应获得 approve');
194
+ const inboxSet = state.updateCalls
195
+ .map(u => u.$set && u.$set.inbox_users)
196
+ .filter(Boolean)[0];
197
+ assert.deepStrictEqual(inboxSet, ['uA', 'uC'],
198
+ '停用账号 uB 不应进入 inbox_users');
199
+ });
200
+
201
+ await run('全员停用 → 中止滑步,实例停留在当前步骤(零写库)', async () => {
202
+ setupCase('counterSign', ['uB'], [
203
+ { user: 'uB', user_accepted: false },
204
+ ]);
205
+
206
+ await UUFlowManager.handleSkipProcessed('ins1', { _id: 'flow1' });
207
+
208
+ assert.strictEqual(state.updateCalls.length, 0,
209
+ '过滤后无人应中止滑步,不得改动实例');
210
+ assert.strictEqual(state.pushedTraces.length, 0, '不应派发新 trace');
211
+ });
212
+
213
+ await run('sign:首位处理人已停用 → 取过滤后的第一个在职账号', async () => {
214
+ setupCase('sign', ['uB', 'uA'], [
215
+ { user: 'uB', user_accepted: false }, // 停用账号占首位
216
+ { user: 'uA', user_accepted: true },
217
+ ]);
218
+
219
+ await UUFlowManager.handleSkipProcessed('ins1', { _id: 'flow1' });
220
+
221
+ assert.strictEqual(state.pushedTraces.length, 1, '应派发出一条新 trace');
222
+ const approveUsers = state.pushedTraces[0].approves.map(a => a.user);
223
+ assert.deepStrictEqual(approveUsers, ['uA'],
224
+ 'sign 步应取过滤后的第一个在职处理人,而不是停用的 uB');
225
+ });
226
+
227
+ await run('无 space_users 记录的处理人视同停用(与 router 口径一致)', async () => {
228
+ setupCase('counterSign', ['uA', 'uX'], [
229
+ { user: 'uA', user_accepted: true },
230
+ // uX 在本 space 无 space_users 记录
231
+ ]);
232
+
233
+ await UUFlowManager.handleSkipProcessed('ins1', { _id: 'flow1' });
234
+
235
+ assert.strictEqual(state.pushedTraces.length, 1);
236
+ const approveUsers = state.pushedTraces[0].approves.map(a => a.user);
237
+ assert.deepStrictEqual(approveUsers, ['uA'],
238
+ '本 space 无 space_users 记录的 uX 不应获得 approve');
239
+ });
240
+
241
+ console.log(`\n${passed} passed, ${failed} failed`);
242
+ process.exit(failed > 0 ? 1 : 0);
243
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steedos-labs/plugin-workflow",
3
- "version": "3.0.106",
3
+ "version": "3.0.107",
4
4
  "main": "package.service.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -26,6 +26,7 @@
26
26
  "test:formula-compat": "node main/default/test/test_formula_compat.js",
27
27
  "test:approve-values": "node main/default/test/test_getApproveValues.js",
28
28
  "test:skip-handler-error": "node main/default/test/test_handleSkipProcessed_handlerError.js",
29
+ "test:skip-user-accepted": "node main/default/test/test_handleSkipProcessed_userAccepted.js",
29
30
  "test:return-after-skip": "node main/default/test/test_return_after_skip.js",
30
31
  "test:retrieve-guards": "node main/default/test/test_retrieve_guards.js",
31
32
  "test:remove-guards": "node main/default/test/test_remove_guards.js",