@stalfh233/omc-fake-platform 0.0.0-stage → 0.4.39

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.
@@ -0,0 +1,363 @@
1
+ import { businessRefusal, malformedEnvelope, NOT_HANDLED } from "./state.js";
2
+ const PENDING_STATES = new Set(["NOTSTARTED", "INPROGRESS"]);
3
+ const ACTIVITY_TYPES = new Set(["START", "END", "CONNECTION", "PARTICIPANT", "SUB_INSTANCE", "CIRCULATE", "SYSTEM_ACTIVITY", "AGENT_ACTIVITY"]);
4
+ const POINT = /^-?\d+(?:\.\d+)?,\s-?\d+(?:\.\d+)?$/;
5
+ /**
6
+ * Live 8.6.26 fidelity (2026-09-17 probe): workflow/validate and /publish
7
+ * answer HTTP 200 + errcode 0 and put the truth in `data` as
8
+ * ValidateMessage[]. The originate (first PARTICIPANT) node's `participant`
9
+ * must be the platform keyword `"{Originator}"`; a plain value ("admin")
10
+ * answers `401035 <activityName> ERROR` and the flow never goes live.
11
+ * `"{Creator}"` on a non-originate participant node is a `401062` WARN that
12
+ * does not block.
13
+ */
14
+ function validateWorkflowParticipants(activities) {
15
+ if (!Array.isArray(activities))
16
+ return [];
17
+ const rows = activities;
18
+ const participants = rows.filter((activity) => String(activity?.activityType ?? "") === "PARTICIPANT");
19
+ const messages = [];
20
+ const originate = participants[0];
21
+ if (originate && String(originate.participant ?? "") !== "{Originator}")
22
+ messages.push({ errCode: 401035, errContent: originate.activityName ?? originate.activityCode ?? "", errMsg: null, errorLevel: "ERROR" });
23
+ for (const activity of participants.slice(1)) {
24
+ if (String(activity.participant ?? "") === "{Creator}")
25
+ messages.push({ errCode: 401062, errContent: activity.activityName ?? activity.activityCode ?? "", errMsg: null, errorLevel: "WARN" });
26
+ }
27
+ return messages;
28
+ }
29
+ function invalidRenderShape(body) {
30
+ if (!Array.isArray(body.activities))
31
+ return "activities";
32
+ const codes = new Set();
33
+ for (const activity of body.activities) {
34
+ const record = activity;
35
+ const code = String(record?.activityCode ?? "").trim();
36
+ const type = String(record?.activityType ?? "").trim();
37
+ if (!code || codes.has(code) || !ACTIVITY_TYPES.has(type))
38
+ return "activities";
39
+ codes.add(code);
40
+ }
41
+ if (!Array.isArray(body.rules))
42
+ return "rules";
43
+ for (const rule of body.rules) {
44
+ const record = rule;
45
+ const pre = String(record?.preActivityCode ?? "");
46
+ const post = String(record?.postActivityCode ?? "");
47
+ const points = record?.points;
48
+ if (!codes.has(pre) || !codes.has(post) || !Array.isArray(points) || points.length === 0 || points.some((point) => typeof point !== "string" || !POINT.test(point)))
49
+ return "rules";
50
+ }
51
+ return null;
52
+ }
53
+ /**
54
+ * Spawn the two work items a workflow start produces: the originator fill item
55
+ * (already FINISHED — the start submit is the fill) and the pending approval
56
+ * item that `workitem.list` must surface for `data.submit`.
57
+ */
58
+ export function startWorkflowInstance(state, input) {
59
+ const sequence = state.workItems.size + 1;
60
+ const instanceId = `wf-${sequence}`;
61
+ const fillId = `wi-${sequence}`;
62
+ const approveId = `wi-${sequence + 1}`;
63
+ state.workItems.set(fillId, {
64
+ id: fillId,
65
+ instanceId,
66
+ activityCode: "fill",
67
+ activityName: "填写申请单",
68
+ participant: "tester",
69
+ state: "FINISHED",
70
+ workflowCode: input.workflowCode,
71
+ schemaCode: input.schemaCode,
72
+ bizObjectId: input.bizObjectId,
73
+ });
74
+ state.workItems.set(approveId, {
75
+ id: approveId,
76
+ instanceId,
77
+ activityCode: "approve",
78
+ activityName: "审批",
79
+ participant: "tester",
80
+ state: "INPROGRESS",
81
+ workflowCode: input.workflowCode,
82
+ schemaCode: input.schemaCode,
83
+ bizObjectId: input.bizObjectId,
84
+ });
85
+ return { workflowInstanceId: instanceId, workItemId: approveId };
86
+ }
87
+ /** Complete a pending work item (approval submit). Returns false if unknown. */
88
+ export function completeWorkItem(state, workItemId) {
89
+ const item = state.workItems.get(workItemId);
90
+ if (!item)
91
+ return false;
92
+ item.state = "FINISHED";
93
+ return true;
94
+ }
95
+ /**
96
+ * Portal draft-save shape (finishStart=FALSE): one instance in DRAFT with a
97
+ * single PENDING start work item. Submitting that item (startWorkflowDraft's
98
+ * workItemId) is what advances the instance — see startWorkflowInstance for
99
+ * the finishStart=TRUE twin.
100
+ */
101
+ export function startWorkflowDraft(state, input) {
102
+ const sequence = state.workItems.size + 1;
103
+ const instanceId = `wf-${sequence}`;
104
+ const startId = `wi-${sequence}`;
105
+ state.workItems.set(startId, {
106
+ id: startId,
107
+ instanceId,
108
+ activityCode: "fill",
109
+ activityName: "填写申请单",
110
+ participant: "tester",
111
+ state: "INPROGRESS",
112
+ workflowCode: input.workflowCode,
113
+ schemaCode: input.schemaCode,
114
+ bizObjectId: input.bizObjectId,
115
+ isStart: true,
116
+ });
117
+ return { workflowInstanceId: instanceId, workItemId: startId };
118
+ }
119
+ /** After the START work item is submitted, the next (approval) activity spawns a pending todo. */
120
+ export function spawnApprovalWorkItem(state, input) {
121
+ const approveId = `wi-${state.workItems.size + 1}`;
122
+ state.workItems.set(approveId, {
123
+ id: approveId,
124
+ instanceId: input.instanceId,
125
+ activityCode: "approve",
126
+ activityName: "审批",
127
+ participant: "tester",
128
+ state: "INPROGRESS",
129
+ workflowCode: input.workflowCode,
130
+ schemaCode: input.schemaCode,
131
+ bizObjectId: input.bizObjectId,
132
+ });
133
+ return approveId;
134
+ }
135
+ function toWorkItemVO(item) {
136
+ return {
137
+ id: item.id,
138
+ instanceId: item.instanceId,
139
+ activityCode: item.activityCode,
140
+ activityName: item.activityName,
141
+ state: item.state,
142
+ workflowCode: item.workflowCode,
143
+ schemaCode: item.schemaCode,
144
+ participant: item.participant,
145
+ participantName: item.participantName ?? item.participant,
146
+ originator: item.originator,
147
+ sequenceNo: item.sequenceNo,
148
+ };
149
+ }
150
+ /** W7-S29-greenfield lane: workflow template authoring + runtime work-item query. Twin of src/lanes/workflow.ts. */
151
+ export const workflowRoute = (ctx, input) => {
152
+ const q = input.query ?? {};
153
+ const body = (input.body ?? {});
154
+ switch (input.path) {
155
+ case "/api/api/workflow/create": {
156
+ const code = String(body.workflowCode ?? body.code ?? "");
157
+ const name = String(body.workflowName ?? body.name ?? code);
158
+ const schemaCode = String(body.schemaCode ?? "");
159
+ // Ticket 76 live fidelity: creating a workflow when the SCHEMA already
160
+ // has one answers an envelope with NO numeric errcode (live:
161
+ // "envelope for /api/api/workflow/create has no numeric errcode") — the
162
+ // tool must decode it as a duplicate-create, not a malformed failure.
163
+ if ([...ctx.state.workflows.values()].some((wf) => wf.schemaCode === schemaCode) && !ctx.state.workflows.has(code))
164
+ return malformedEnvelope({ workflowCode: code, workflowName: name, schemaCode, exist: true });
165
+ if (!ctx.state.workflows.has(code)) {
166
+ // Ticket 112 platform fidelity: the real create auto-generates a
167
+ // full draft (START → PARTICIPANT 填写申请单 → END with canvas rules;
168
+ // WorkflowTemplateController update @ApiImplicitParam example) — the
169
+ // fake seeds the same platform-shaped template so the merge path has
170
+ // real activity boxes and rule shapes to echo.
171
+ ctx.state.workflows.set(code, {
172
+ code,
173
+ name,
174
+ schemaCode,
175
+ template: {
176
+ workflowCode: code,
177
+ name,
178
+ workflowName: name,
179
+ schemaCode,
180
+ // Live create seeds templateType DRAFT ("SAVE" is not a
181
+ // WorkflowTemplateStatus member — S29 contract).
182
+ templateType: "DRAFT",
183
+ activities: [
184
+ { activityCode: "Activity1", activityType: "START", activityName: "开始", x: 327, y: 36, width: 158, height: 40 },
185
+ { activityCode: "Activity2", activityType: "PARTICIPANT", activityName: "填写申请单", participant: "{Originator}", x: 327, y: 116, width: 158, height: 40 },
186
+ { activityCode: "Activity3", activityType: "END", activityName: "结束", x: 327, y: 196, width: 158, height: 40 },
187
+ ],
188
+ rules: [
189
+ { preActivityCode: "Activity1", postActivityCode: "Activity2", points: ["406, 76", "406, 116"] },
190
+ { preActivityCode: "Activity2", postActivityCode: "Activity3", points: ["406, 156", "406, 196"] },
191
+ ],
192
+ },
193
+ published: null,
194
+ });
195
+ }
196
+ return { code };
197
+ }
198
+ case "/api/api/workflow/isworkflow": {
199
+ // WorkflowTemplateController.isWorkFlow(schemaCode): any header for the
200
+ // schema answers true.
201
+ const schemaCode = String(q.schemaCode ?? "");
202
+ return [...ctx.state.workflows.values()].some((wf) => wf.schemaCode === schemaCode);
203
+ }
204
+ case "/api/api/workflow/getWorkflow": {
205
+ // WorkflowTemplateController.getWorkflow(schemaCode): the published
206
+ // template for the schema (carries workflowCode), or null when the model
207
+ // has none. Used to auto-start a workflow-backed data.submit.
208
+ const schemaCode = String(q.schemaCode ?? "");
209
+ const wf = [...ctx.state.workflows.values()].find((candidate) => candidate.schemaCode === schemaCode);
210
+ if (!wf)
211
+ return null;
212
+ const template = (wf.published ?? wf.template);
213
+ return { ...template, workflowCode: wf.code };
214
+ }
215
+ case "/api/api/workflow/get_header": {
216
+ const wf = ctx.state.workflows.get(String(q.workflowCode ?? ""));
217
+ // Round-2 live fidelity (BUG-05): these endpoints answer 401001
218
+ // 流程不存在! for an ABSENT workflow rather than a null payload.
219
+ if (!wf)
220
+ return businessRefusal(401001, "流程不存在!");
221
+ return { code: wf.code, name: wf.name };
222
+ }
223
+ case "/api/api/workflow/get_draft": {
224
+ const wf = ctx.state.workflows.get(String(q.workflowCode ?? ""));
225
+ if (!wf)
226
+ return businessRefusal(401001, "流程不存在!");
227
+ return wf.template;
228
+ }
229
+ case "/api/api/workflow/get_published": {
230
+ const wf = ctx.state.workflows.get(String(q.workflowCode ?? ""));
231
+ // Live fidelity: like get_header/get_draft, an ABSENT workflow answers
232
+ // 401001 流程不存在! rather than a null payload; the tool degrades that
233
+ // to exists:false. Ticket 77: an UNPUBLISHED workflow still answers the
234
+ // DRAFT shape here — data presence is not publish evidence. The
235
+ // published face carries the templateType PUBLISH marker.
236
+ if (!wf)
237
+ return businessRefusal(401001, "流程不存在!");
238
+ return wf.published ?? wf.template ?? null;
239
+ }
240
+ case "/api/api/workflow/update": {
241
+ const code = String(body.workflowCode ?? q.workflowCode ?? "");
242
+ const wf = ctx.state.workflows.get(code);
243
+ if (!wf)
244
+ return null;
245
+ // Ticket 112 platform fidelity (WorkflowTemplateUtil.java:90-171): an
246
+ // illegal templateType ("BusinessProcess" is not a
247
+ // WorkflowTemplateStatus member — valueOf at :136) or an activities
248
+ // element without a type makes the converter swallow the exception
249
+ // into a PARTIAL model; the save chain then fails with errcode 50000.
250
+ if (body.templateType === "BusinessProcess")
251
+ return businessRefusal(50000, "服务异常");
252
+ if (Array.isArray(body.activities) &&
253
+ body.activities.some((activity) => {
254
+ const record = activity;
255
+ const type = record != null && typeof record === "object" ? (record.type ?? record.activityType) : undefined;
256
+ return type === undefined || type === null || String(type).trim() === "";
257
+ }))
258
+ return businessRefusal(50000, "服务异常");
259
+ if (body.activities !== undefined && invalidRenderShape(body))
260
+ return businessRefusal(50000, "服务异常");
261
+ // Platform fidelity (live 2026-09-23; bench12/13/15): an existing
262
+ // activityCode cannot be RENAMED or RETYPED — the stored name/type win.
263
+ // A caller that wants a different name/type must send it under a fresh
264
+ // code (the tool reconciles this before the wire); re-sending the same
265
+ // code with a changed name is a silent no-op on the platform, so the fake
266
+ // must reproduce it rather than store the body verbatim.
267
+ const stored = wf.template.activities;
268
+ const storedByCode = new Map((Array.isArray(stored) ? stored : []).map((activity) => [String(activity.activityCode ?? ""), activity]));
269
+ const pinned = Array.isArray(body.activities)
270
+ ? {
271
+ ...body,
272
+ activities: body.activities.map((activity) => {
273
+ const prior = storedByCode.get(String(activity.activityCode ?? ""));
274
+ return prior
275
+ ? { ...activity, activityName: prior.activityName, i18nMessage: prior.i18nMessage, activityType: prior.activityType }
276
+ : activity;
277
+ }),
278
+ }
279
+ : body;
280
+ wf.template = { ...pinned };
281
+ return { code };
282
+ }
283
+ case "/api/api/workflow/validate":
284
+ case "/api/api/workflow/publish": {
285
+ const code = String(body.workflowCode ?? q.workflowCode ?? "");
286
+ const wf = ctx.state.workflows.get(code);
287
+ if (!wf)
288
+ return null;
289
+ if (body.activities !== undefined && invalidRenderShape(body))
290
+ return businessRefusal(50000, "服务异常");
291
+ // Live 8.6.26 fidelity (2026-09-17): validate/publish REFUSE with HTTP
292
+ // 200 + errcode 0 and the truth in `data` as ValidateMessage[]
293
+ // ({errCode, errContent, errorLevel}). The originate (first PARTICIPANT)
294
+ // node's participant must be the platform keyword "{Originator}" — a
295
+ // plain user/role value answers 401035 ERROR while errcode stays 0, and
296
+ // the flow never goes live. `{Creator}` on a non-originate node is a WARN
297
+ // (401062) that does not block.
298
+ const activities = Array.isArray(body.activities) ? body.activities : wf.template.activities;
299
+ const validation = validateWorkflowParticipants(activities);
300
+ if (input.path === "/api/api/workflow/validate")
301
+ return validation;
302
+ if (validation.some((message) => message.errorLevel === "ERROR"))
303
+ return validation;
304
+ // Ticket 77: the published face carries the PUBLISH marker (live readback
305
+ // evidence: publish state is identified by templateType:PUBLISH /
306
+ // version fields, never by data presence).
307
+ wf.published = { ...body, templateType: "PUBLISH", workflowVersion: (wf.published ? Number(wf.published.workflowVersion ?? 1) : 0) + 1 };
308
+ return { code, workflowVersion: wf.published.workflowVersion };
309
+ }
310
+ case "/api/api/workflow/delete":
311
+ ctx.state.workflows.delete(String(body.workflowCode ?? q.workflowCode ?? q.code ?? ""));
312
+ return null;
313
+ case "/api/api/runtime/workflow/search_workitems": {
314
+ // The real endpoint returns the caller's pending todos (biz_workitem only
315
+ // holds pending rows); finished items move to the finished store. The
316
+ // server's filterWorkflowInstanceId is a NOT-equal exclusion, so it is
317
+ // deliberately not honored here — exact instance matching is client-side.
318
+ let rows = [...ctx.state.workItems.values()].filter((item) => PENDING_STATES.has(item.state));
319
+ if (q.workflowCode)
320
+ rows = rows.filter((item) => item.workflowCode === q.workflowCode);
321
+ if (q.schemaCode)
322
+ rows = rows.filter((item) => item.schemaCode === q.schemaCode);
323
+ const page = Number(q.page ?? 0);
324
+ const size = Number(q.size ?? 20);
325
+ const start = page * size;
326
+ return {
327
+ totalElements: rows.length,
328
+ totalPages: size > 0 ? Math.ceil(rows.length / size) : 0,
329
+ content: rows.slice(start, start + size).map(toWorkItemVO),
330
+ };
331
+ }
332
+ case "/api/api/runtime/workflow/getBizObjectByWfInstanceId": {
333
+ const instanceId = String(q.workflowInstanceId ?? "");
334
+ const item = [...ctx.state.workItems.values()].find((candidate) => candidate.instanceId === instanceId);
335
+ if (!item)
336
+ return null;
337
+ return { id: item.bizObjectId, schemaCode: item.schemaCode };
338
+ }
339
+ case "/api/api/organization/role/list":
340
+ // Real shape: groups with `children` roles (see real-env observation).
341
+ return [
342
+ { id: "grp-1", name: "默认", code: "SYS_1000000", children: [{ id: "role-lead", name: "部门负责人", code: "SYS260630023816197", roleType: "SYS", corpId: "corp-1" }] },
343
+ { id: "role-finance", name: "财务负责人", code: "SYS260515031207258", roleType: "SYS", corpId: "corp-1" },
344
+ ];
345
+ case "/api/api/organization/department/tree":
346
+ return { departmentList: [{ id: "dept-1", name: "总所", code: "1", corpId: "corp-1", parentId: null }, { id: "dept-2", name: "业务部", code: "1_1", corpId: "corp-1", parentId: "dept-1" }] };
347
+ case "/api/api/organization/user/search": {
348
+ // UserController.search: @RequestBody {wd,page,size} → PageVO<UserVO>.
349
+ const wd = String(body.wd ?? "");
350
+ if (!wd)
351
+ return businessRefusal(10005, "关键字不能为空");
352
+ const users = [
353
+ { id: "user-1", username: "18900000001", name: "付文彬", departmentId: "dept-1", userId: "18900000001", status: "ENABLE", deleted: false, corpId: "corp-1" },
354
+ { id: "user-2", username: "18900000002", name: "李四", departmentId: "dept-2", userId: "18900000002", status: "ENABLE", deleted: false, corpId: "corp-1" },
355
+ ].filter((user) => user.name.includes(wd) || user.username.includes(wd));
356
+ const page = Number(body.page ?? 0);
357
+ const size = Number(body.size ?? 100);
358
+ return { totalElements: users.length, totalPages: size > 0 ? Math.ceil(users.length / size) : 0, content: users.slice(page * size, page * size + size) };
359
+ }
360
+ default:
361
+ return NOT_HANDLED;
362
+ }
363
+ };
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-fake-platform",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.39",
4
+ "description": "OMC 的内存假 CloudPivot 平台(wire 接缝的第二个适配器)+ HTTP 假服务器——@stalfh233/omc-cli 的测试与 bench 基准基座",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./state": "./dist/state.js",
10
+ "./http-server": "./dist/http-server.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "package.json",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc --project tsconfig.build.json"
19
+ },
20
+ "dependencies": {
21
+ "omc-core": "0.4.39"
22
+ },
23
+ "engines": {
24
+ "node": ">=22.13"
25
+ }
26
+ }