@dsh-workbench/shared 1.1.8 → 1.1.10

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.
Files changed (226) hide show
  1. package/dist/api/blocker-table.d.ts +1 -0
  2. package/dist/api/blocker-table.js +1 -1
  3. package/dist/components/SelectField.css +0 -6
  4. package/dist/components/SelectField.d.ts +2 -3
  5. package/dist/components/SelectField.js +9 -30
  6. package/dist/components/SelectFieldNodes.d.ts +3 -1
  7. package/dist/components/SelectFieldNodes.js +2 -1
  8. package/dist/components/SelectFieldWheel.d.ts +9 -0
  9. package/dist/components/SelectFieldWheel.js +35 -0
  10. package/dist/config/constants.d.ts +4 -4
  11. package/dist/config/constants.js +4 -4
  12. package/dist/contract/capabilities.d.ts +29 -0
  13. package/dist/contract/capabilities.js +1 -0
  14. package/dist/contract/needs.d.ts +3 -3
  15. package/dist/contract/needs.js +1 -1
  16. package/dist/contract/plugin-source.d.ts +19 -0
  17. package/dist/contract/protocol/bundle-install.d.ts +3 -1
  18. package/dist/contract/protocol/bundle-install.js +3 -0
  19. package/dist/contract/protocol/market-index.d.ts +1 -1
  20. package/dist/contract/protocol/platform.d.ts +1 -1
  21. package/dist/contract/runner.d.ts +6 -0
  22. package/dist/contract/runner.js +1 -0
  23. package/dist/contract/unit.d.ts +4 -4
  24. package/dist/host/adapter-contract.d.ts +3 -0
  25. package/dist/host/adapter-contract.js +93 -0
  26. package/dist/host/adapter-set.js +6 -4
  27. package/dist/host/adapter-types.d.ts +1 -1
  28. package/dist/host/bundle-manager.d.ts +3 -0
  29. package/dist/host/bundle-manager.js +181 -0
  30. package/dist/host/cli.js +3 -1
  31. package/dist/host/config-sync.d.ts +48 -0
  32. package/dist/host/config-sync.js +342 -0
  33. package/dist/host/dsh-plugins.d.ts +5 -0
  34. package/dist/host/dsh-plugins.js +51 -0
  35. package/dist/host/extension.js +1 -1
  36. package/dist/host/flow/check.d.ts +9 -0
  37. package/dist/host/flow/check.js +179 -0
  38. package/dist/host/flow/expr.d.ts +30 -0
  39. package/dist/host/flow/expr.js +99 -0
  40. package/dist/host/flow/kit.d.ts +14 -0
  41. package/dist/host/flow/kit.js +13 -0
  42. package/dist/host/flow/run.d.ts +13 -0
  43. package/dist/host/flow/run.js +287 -0
  44. package/dist/host/flow/types.d.ts +188 -0
  45. package/dist/host/flow/types.js +1 -0
  46. package/dist/host/host-actions.d.ts +2 -0
  47. package/dist/host/host-actions.js +5 -0
  48. package/dist/host/host-kit.d.ts +19 -0
  49. package/dist/host/host-kit.js +14 -0
  50. package/dist/host/http.d.ts +2 -6
  51. package/dist/host/http.js +2 -2
  52. package/dist/host/installed-module.d.ts +23 -0
  53. package/dist/host/installed-module.js +264 -0
  54. package/dist/host/lib-install.d.ts +7 -9
  55. package/dist/host/lib-install.js +9 -12
  56. package/dist/host/lib-runners.d.ts +3 -5
  57. package/dist/host/lib-runners.js +2 -2
  58. package/dist/host/local-declaration.d.ts +7 -0
  59. package/dist/host/local-declaration.js +33 -0
  60. package/dist/host/module-root.d.ts +1 -0
  61. package/dist/host/module-root.js +5 -0
  62. package/dist/host/npm-registry.d.ts +10 -0
  63. package/dist/host/npm-registry.js +16 -0
  64. package/dist/host/opencli-runner.d.ts +2 -1
  65. package/dist/host/package-content.d.ts +7 -0
  66. package/dist/host/package-content.js +69 -0
  67. package/dist/host/package-kind.d.ts +13 -0
  68. package/dist/host/package-kind.js +51 -0
  69. package/dist/host/package-route.d.ts +31 -0
  70. package/dist/host/package-route.js +57 -0
  71. package/dist/host/paths.d.ts +11 -19
  72. package/dist/host/paths.js +16 -43
  73. package/dist/host/platform.js +1 -1
  74. package/dist/host/plugin-ledger.d.ts +8 -8
  75. package/dist/host/plugin-ledger.js +23 -16
  76. package/dist/host/plugin.d.ts +1 -2
  77. package/dist/host/plugin.js +4 -5
  78. package/dist/host/port-types.d.ts +42 -9
  79. package/dist/host/preflight.d.ts +3 -2
  80. package/dist/host/profile-create.d.ts +18 -0
  81. package/dist/host/profile-create.js +118 -0
  82. package/dist/host/profile-registries.d.ts +10 -0
  83. package/dist/host/profile-registries.js +150 -0
  84. package/dist/host/registry-declaration.d.ts +21 -0
  85. package/dist/host/registry-declaration.js +33 -0
  86. package/dist/host/release-probe.d.ts +34 -0
  87. package/dist/host/release-probe.js +88 -0
  88. package/dist/host/runner.d.ts +26 -0
  89. package/dist/host/runner.js +50 -0
  90. package/dist/host/shell.d.ts +0 -2
  91. package/dist/host/shell.js +0 -7
  92. package/dist/host/spec.js +1 -1
  93. package/dist/host/supply-index.d.ts +1 -0
  94. package/dist/host/supply-index.js +8 -7
  95. package/dist/host/supply-port.js +13 -3
  96. package/dist/host/unit-face.d.ts +2 -1
  97. package/dist/host/version.js +11 -6
  98. package/dist/host.d.ts +24 -6
  99. package/dist/host.js +23 -6
  100. package/dist/i18n/en.js +23 -24
  101. package/dist/i18n/index.d.ts +23 -24
  102. package/dist/i18n/runtime-terms.d.ts +15 -0
  103. package/dist/i18n/runtime-terms.js +22 -0
  104. package/dist/i18n/zh.d.ts +22 -23
  105. package/dist/i18n/zh.js +22 -23
  106. package/dist/index.d.ts +4 -0
  107. package/dist/index.js +4 -0
  108. package/dist/testing/fake-adapter-set.d.ts +5 -0
  109. package/dist/testing/fake-adapter-set.js +150 -0
  110. package/dist/testing/needs.js +1 -1
  111. package/dist/testing.d.ts +1 -0
  112. package/dist/testing.js +1 -0
  113. package/package.json +1 -1
  114. package/src/api/__tests__/blocker-table.test.ts +25 -0
  115. package/src/api/{workbench-client.test.ts → __tests__/workbench-client.test.ts} +2 -2
  116. package/src/api/blocker-table.ts +6 -2
  117. package/src/components/SelectField.css +0 -6
  118. package/src/components/SelectField.tsx +7 -36
  119. package/src/components/SelectFieldNodes.tsx +4 -1
  120. package/src/components/{Card.test.tsx → __tests__/Card.test.tsx} +2 -2
  121. package/src/components/{CardAnchor.test.tsx → __tests__/CardAnchor.test.tsx} +3 -3
  122. package/src/components/{CardHead.test.tsx → __tests__/CardHead.test.tsx} +1 -1
  123. package/src/components/{ChoiceGroup.test.tsx → __tests__/ChoiceGroup.test.tsx} +2 -2
  124. package/src/components/{ConfirmBar.test.tsx → __tests__/ConfirmBar.test.tsx} +5 -5
  125. package/src/components/{Field.test.tsx → __tests__/Field.test.tsx} +1 -1
  126. package/src/components/{Form.test.tsx → __tests__/Form.test.tsx} +5 -5
  127. package/src/components/{FormActions.test.tsx → __tests__/FormActions.test.tsx} +3 -3
  128. package/src/components/{FormRows.test.tsx → __tests__/FormRows.test.tsx} +2 -2
  129. package/src/components/{FormTypes.test.ts → __tests__/FormTypes.test.ts} +1 -1
  130. package/src/components/{FormWiring.test.tsx → __tests__/FormWiring.test.tsx} +4 -4
  131. package/src/components/{Link.test.tsx → __tests__/Link.test.tsx} +1 -1
  132. package/src/components/{Rows.test.tsx → __tests__/Rows.test.tsx} +2 -2
  133. package/src/components/{SelectField.test.tsx → __tests__/SelectField.test.tsx} +7 -37
  134. package/src/components/{Spinner.test.tsx → __tests__/Spinner.test.tsx} +4 -4
  135. package/src/config/{constants.test.ts → __tests__/constants.test.ts} +1 -1
  136. package/src/config/{storage.test.ts → __tests__/storage.test.ts} +1 -1
  137. package/src/config/constants.ts +4 -4
  138. package/src/contract/capabilities.ts +30 -0
  139. package/src/contract/needs.ts +4 -4
  140. package/src/contract/plugin-source.ts +24 -0
  141. package/src/contract/protocol/bundle-install.ts +7 -1
  142. package/src/contract/protocol/market-index.ts +1 -0
  143. package/src/contract/protocol/platform.ts +1 -1
  144. package/src/contract/runner.ts +6 -0
  145. package/src/contract/unit.ts +4 -4
  146. package/src/host/{extension.test.ts → __tests__/extension.test.ts} +4 -4
  147. package/src/host/{install-failure.test.ts → __tests__/install-failure.test.ts} +1 -1
  148. package/src/host/{installed-sources.test.ts → __tests__/installed-sources.test.ts} +1 -1
  149. package/src/host/__tests__/lib-runners.test.ts +11 -2
  150. package/src/host/__tests__/plugin-ledger.test.ts +19 -6
  151. package/src/host/{recipe.test.ts → __tests__/recipe.test.ts} +3 -3
  152. package/src/host/__tests__/runtime-layout.test.ts +14 -0
  153. package/src/host/__tests__/supply-index.test.ts +2 -2
  154. package/src/host/__tests__/supply-port.test.ts +32 -0
  155. package/src/host/adapter-contract.ts +93 -0
  156. package/src/host/adapter-set.ts +6 -4
  157. package/src/host/adapter-types.ts +1 -1
  158. package/src/host/bundle-manager.ts +195 -0
  159. package/src/host/cli.ts +3 -1
  160. package/src/host/config-sync.ts +386 -0
  161. package/src/host/dsh-plugins.ts +60 -0
  162. package/src/host/extension.ts +1 -1
  163. package/src/host/flow/__tests__/advanced.test.ts +302 -0
  164. package/src/host/flow/__tests__/check.test.ts +169 -0
  165. package/src/host/flow/__tests__/run.test.ts +301 -0
  166. package/src/host/flow/check.ts +216 -0
  167. package/src/host/flow/expr.ts +94 -0
  168. package/src/host/flow/kit.ts +30 -0
  169. package/src/host/flow/run.ts +325 -0
  170. package/src/host/flow/types.ts +199 -0
  171. package/src/host/host-actions.ts +7 -0
  172. package/src/host/host-kit.ts +26 -0
  173. package/src/host/http.ts +6 -8
  174. package/src/host/installed-module.ts +272 -0
  175. package/src/host/lib-install.ts +14 -12
  176. package/src/host/lib-runners.ts +6 -8
  177. package/src/host/local-declaration.ts +37 -0
  178. package/src/host/module-root.ts +6 -0
  179. package/src/host/npm-registry.ts +21 -0
  180. package/src/host/package-content.ts +76 -0
  181. package/src/host/package-kind.ts +63 -0
  182. package/src/host/package-route.ts +93 -0
  183. package/src/host/paths.ts +27 -52
  184. package/src/host/platform.ts +1 -1
  185. package/src/host/plugin-ledger.ts +25 -18
  186. package/src/host/plugin.ts +4 -5
  187. package/src/host/port-types.ts +43 -9
  188. package/src/host/preflight.ts +3 -2
  189. package/src/host/profile-create.ts +133 -0
  190. package/src/host/profile-registries.ts +145 -0
  191. package/src/host/registry-declaration.ts +70 -0
  192. package/src/host/release-probe.ts +202 -0
  193. package/src/host/shell.ts +0 -9
  194. package/src/host/spec.ts +1 -1
  195. package/src/host/supply-index.ts +9 -7
  196. package/src/host/supply-port.ts +13 -3
  197. package/src/host/unit-face.ts +2 -1
  198. package/src/host/version.ts +12 -7
  199. package/src/host.ts +34 -6
  200. package/src/i18n/{index.test.ts → __tests__/index.test.ts} +3 -3
  201. package/src/i18n/{merge.test.ts → __tests__/merge.test.ts} +1 -1
  202. package/src/i18n/{zh.test.ts → __tests__/zh.test.ts} +2 -2
  203. package/src/i18n/en.ts +23 -25
  204. package/src/i18n/runtime-terms.ts +26 -0
  205. package/src/i18n/zh.ts +22 -24
  206. package/src/index.ts +4 -0
  207. package/src/lib/{board-fact.test.ts → __tests__/board-fact.test.ts} +1 -1
  208. package/src/lib/{busy.test.ts → __tests__/busy.test.ts} +1 -1
  209. package/src/lib/{form-problem.test.ts → __tests__/form-problem.test.ts} +1 -1
  210. package/src/lib/{git-address.test.ts → __tests__/git-address.test.ts} +1 -1
  211. package/src/lib/{react.test.ts → __tests__/react.test.ts} +1 -1
  212. package/src/lib/{text.test.ts → __tests__/text.test.ts} +1 -1
  213. package/src/lib/{ui.test.ts → __tests__/ui.test.ts} +1 -1
  214. package/src/testing/__tests__/fake-adapter-set.test.ts +28 -0
  215. package/src/testing/fake-adapter-set.ts +173 -0
  216. package/src/testing/needs.ts +1 -1
  217. package/src/testing.ts +1 -0
  218. package/src/host/__tests__/command-name.test.ts +0 -16
  219. package/src/host/__tests__/opencli-runner.test.ts +0 -120
  220. package/src/host/channel-plugin.ts +0 -55
  221. package/src/host/command-name.ts +0 -15
  222. package/src/host/opencli-runner.ts +0 -76
  223. package/src/host/paths.test.ts +0 -26
  224. package/src/host/plugin-source.ts +0 -137
  225. package/src/host/runtime-links.test.ts +0 -31
  226. package/src/host/runtime-links.ts +0 -23
@@ -0,0 +1,302 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createFlowActionRegistry, runFlow } from '../run.js';
3
+ import type {
4
+ FlowAction,
5
+ FlowActionRegistry,
6
+ FlowDefinition,
7
+ FlowResult,
8
+ FlowStepEvent,
9
+ FlowValue,
10
+ } from '../types.js';
11
+
12
+ const ref = (path: string): FlowValue => ({ $ref: path });
13
+ const lit = (value: unknown): FlowValue => ({ $lit: value });
14
+
15
+ function actionsOf(entries: Readonly<Record<string, FlowAction>>): FlowActionRegistry {
16
+ const registry = createFlowActionRegistry();
17
+ for (const [name, action] of Object.entries(entries)) registry.register(name, action);
18
+ return registry;
19
+ }
20
+
21
+ interface RunExtra {
22
+ readonly signal?: AbortSignal | undefined;
23
+ readonly sleep?: ((ms: number, signal: AbortSignal) => Promise<void>) | undefined;
24
+ readonly now?: (() => number) | undefined;
25
+ }
26
+
27
+ async function runFlowWith(
28
+ flow: FlowDefinition,
29
+ actions: FlowActionRegistry,
30
+ input: unknown = {},
31
+ extra: RunExtra = {},
32
+ ): Promise<{ result: FlowResult; events: FlowStepEvent[] }> {
33
+ const events: FlowStepEvent[] = [];
34
+ const result = await runFlow({
35
+ flow,
36
+ input,
37
+ actions,
38
+ signal: extra.signal ?? new AbortController().signal,
39
+ runId: 'run-1',
40
+ workspace: '/w/a',
41
+ onStep: (event) => events.push(event),
42
+ ...(extra.sleep === undefined ? {} : { sleep: extra.sleep }),
43
+ ...(extra.now === undefined ? {} : { now: extra.now }),
44
+ });
45
+ return { result, events };
46
+ }
47
+
48
+ describe('runFlow advanced', () => {
49
+ it('forEach 里每项单独 poll', async () => {
50
+ const calls = new Map<string, number>();
51
+ const actions = actionsOf({
52
+ job: async (input) => {
53
+ const id = (input as { id: string }).id;
54
+ const count = (calls.get(id) ?? 0) + 1;
55
+ calls.set(id, count);
56
+ return { id, done: count >= 2 };
57
+ },
58
+ });
59
+
60
+ let clock = 0;
61
+ const { result } = await runFlowWith(
62
+ {
63
+ id: 'each-poll',
64
+ title: '逐项轮询',
65
+ steps: [
66
+ {
67
+ id: 'job',
68
+ action: 'job',
69
+ forEach: { over: ref('in.ids'), as: 'item', max: 3 },
70
+ with: { id: ref('item') },
71
+ poll: {
72
+ everyMs: 100,
73
+ maxMs: 1000,
74
+ until: { $eq: [ref('value.done'), lit(true)] },
75
+ },
76
+ },
77
+ ],
78
+ },
79
+ actions,
80
+ { ids: ['a', 'b'] },
81
+ {
82
+ now: () => clock,
83
+ sleep: async (ms) => {
84
+ clock += ms;
85
+ },
86
+ },
87
+ );
88
+
89
+ expect(result.status).toBe('ok');
90
+ expect(result.steps['job']).toEqual([
91
+ { id: 'a', done: true },
92
+ { id: 'b', done: true },
93
+ ]);
94
+ expect([...calls.values()]).toEqual([2, 2]);
95
+ });
96
+
97
+ it('poll.onTimeout=continue:超时写 undefined,后续 when 决定', async () => {
98
+ const actions = actionsOf({
99
+ find: async () => null,
100
+ fallback: async () => 'triggered',
101
+ });
102
+
103
+ let clock = 0;
104
+ const { result } = await runFlowWith(
105
+ {
106
+ id: 'fallback',
107
+ title: '兜底',
108
+ steps: [
109
+ {
110
+ id: 'find',
111
+ action: 'find',
112
+ poll: {
113
+ everyMs: 100,
114
+ maxMs: 250,
115
+ onTimeout: 'continue',
116
+ until: { $exists: ref('value') },
117
+ },
118
+ },
119
+ {
120
+ id: 'fallback',
121
+ action: 'fallback',
122
+ when: { $not: { $exists: ref('steps.find') } },
123
+ },
124
+ ],
125
+ },
126
+ actions,
127
+ {},
128
+ {
129
+ now: () => clock,
130
+ sleep: async (ms) => {
131
+ clock += ms;
132
+ },
133
+ },
134
+ );
135
+
136
+ expect(result.status).toBe('ok');
137
+ expect(result.steps['find']).toBeUndefined();
138
+ expect(result.steps['fallback']).toBe('triggered');
139
+ });
140
+
141
+ it('poll.maxMs=0 表示不限时', async () => {
142
+ let calls = 0;
143
+ const actions = actionsOf({
144
+ job: async () => {
145
+ calls += 1;
146
+ return calls < 3 ? { done: false } : { done: true };
147
+ },
148
+ });
149
+
150
+ let clock = 0;
151
+ const { result } = await runFlowWith(
152
+ {
153
+ id: 'unlimited',
154
+ title: '不限',
155
+ steps: [
156
+ {
157
+ id: 'job',
158
+ action: 'job',
159
+ poll: {
160
+ everyMs: 100,
161
+ maxMs: 0,
162
+ until: { $eq: [ref('value.done'), lit(true)] },
163
+ },
164
+ },
165
+ ],
166
+ },
167
+ actions,
168
+ {},
169
+ {
170
+ now: () => clock,
171
+ sleep: async (ms) => {
172
+ clock += ms;
173
+ },
174
+ },
175
+ );
176
+
177
+ expect(result.status).toBe('ok');
178
+ expect(calls).toBe(3);
179
+ });
180
+
181
+ it('动作能发 needs-login 事件,流程继续', async () => {
182
+ const actions = actionsOf({
183
+ auth: async (_input, context) => {
184
+ context.login('https://example.com/login');
185
+ return 'ok';
186
+ },
187
+ });
188
+
189
+ const { result, events } = await runFlowWith(
190
+ { id: 'login', title: '登录', steps: [{ id: 'auth', action: 'auth' }] },
191
+ actions,
192
+ );
193
+
194
+ expect(result.status).toBe('ok');
195
+ expect(events).toContainEqual({
196
+ stepId: 'auth',
197
+ title: 'auth',
198
+ status: 'needs-login',
199
+ loginUrl: 'https://example.com/login',
200
+ });
201
+ });
202
+
203
+ it('子流程:with 作为 in,结果挂 steps.<id>,事件带前缀', async () => {
204
+ const actions = actionsOf({
205
+ read: async (input) => ({ mergedSha: (input as { sha: string }).sha }),
206
+ });
207
+ const merge: FlowDefinition = {
208
+ id: 'merge',
209
+ title: '合并',
210
+ steps: [{ id: 'read', action: 'read', with: { sha: ref('in.sha') } }],
211
+ };
212
+ const after: FlowDefinition = {
213
+ id: 'after',
214
+ title: '后续',
215
+ steps: [{ id: 'use', action: 'read', with: { sha: ref('in.sha') } }],
216
+ };
217
+
218
+ const { result, events } = await runFlowWith(
219
+ {
220
+ id: 'publish',
221
+ title: '发布',
222
+ steps: [
223
+ { id: 'merge', title: '合并', flow: merge, with: { sha: ref('in.sha') } },
224
+ {
225
+ id: 'after',
226
+ title: '后续',
227
+ flow: after,
228
+ with: { sha: ref('steps.merge.read.mergedSha') },
229
+ },
230
+ ],
231
+ },
232
+ actions,
233
+ { sha: 'abc' },
234
+ );
235
+
236
+ expect(result.status).toBe('ok');
237
+ expect(result.steps['merge']).toEqual({ read: { mergedSha: 'abc' } });
238
+ expect(result.steps['after']).toEqual({ use: { mergedSha: 'abc' } });
239
+ expect(events.some((event) => event.stepId === 'merge.read' && event.status === 'ok')).toBe(
240
+ true,
241
+ );
242
+ });
243
+
244
+ it('子流程失败:failedStep 带外层步骤前缀', async () => {
245
+ const actions = actionsOf({
246
+ boom: async () => {
247
+ throw new Error('boom');
248
+ },
249
+ });
250
+
251
+ const { result } = await runFlowWith(
252
+ {
253
+ id: 'outer',
254
+ title: '外层',
255
+ steps: [
256
+ {
257
+ id: 'sub',
258
+ title: '子',
259
+ flow: {
260
+ id: 'sub',
261
+ title: '子',
262
+ steps: [{ id: 'boom', action: 'boom' }],
263
+ },
264
+ },
265
+ ],
266
+ },
267
+ actions,
268
+ );
269
+
270
+ expect(result.status).toBe('failed');
271
+ expect(result.failedStep).toBe('sub.boom');
272
+ expect(result.error).toBeInstanceOf(Error);
273
+ });
274
+
275
+ it('stopWhen:满足条件后不再跑后面的步骤', async () => {
276
+ let after = 0;
277
+ const actions = actionsOf({
278
+ read: async () => 'ok',
279
+ after: async () => {
280
+ after += 1;
281
+ return 'no';
282
+ },
283
+ });
284
+
285
+ const { result } = await runFlowWith(
286
+ {
287
+ id: 'stop',
288
+ title: '提前停',
289
+ stopWhen: { $exists: ref('steps.read') },
290
+ steps: [
291
+ { id: 'read', action: 'read' },
292
+ { id: 'after', action: 'after' },
293
+ ],
294
+ },
295
+ actions,
296
+ );
297
+
298
+ expect(result.status).toBe('ok');
299
+ expect(after).toBe(0);
300
+ expect(result.steps['read']).toBe('ok');
301
+ });
302
+ });
@@ -0,0 +1,169 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { checkFlow } from '../check.js';
3
+ import type { FlowDefinition, FlowValue } from '../types.js';
4
+
5
+ const ref = (path: string): FlowValue => ({ $ref: path });
6
+ const lit = (value: unknown): FlowValue => ({ $lit: value });
7
+
8
+ const actions = (names: readonly string[]) => ({
9
+ has: (name: string) => names.includes(name),
10
+ });
11
+
12
+ describe('checkFlow', () => {
13
+ it('合格流程没有 problems', () => {
14
+ const flow: FlowDefinition = {
15
+ id: 'ok',
16
+ title: '合格',
17
+ steps: [
18
+ { id: 'read', action: 'read' },
19
+ {
20
+ id: 'write',
21
+ action: 'write',
22
+ with: { rows: ref('steps.read.rows') },
23
+ retry: { attempts: 2 },
24
+ },
25
+ {
26
+ id: 'wait',
27
+ action: 'read',
28
+ poll: {
29
+ everyMs: 500,
30
+ maxMs: 10_000,
31
+ until: { $eq: [ref('value.status'), lit('done')] },
32
+ },
33
+ },
34
+ ],
35
+ };
36
+
37
+ expect(checkFlow(flow, actions(['read', 'write']))).toEqual({ ok: true, problems: [] });
38
+ });
39
+
40
+ it('未知动作、引用未来步骤、无上限轮询都指到路径', () => {
41
+ const flow: FlowDefinition = {
42
+ id: 'bad',
43
+ title: '坏',
44
+ steps: [
45
+ { id: 'a', action: 'missing' },
46
+ { id: 'b', action: 'read', with: { x: ref('steps.c.y') } },
47
+ {
48
+ id: 'c',
49
+ action: 'read',
50
+ poll: {
51
+ everyMs: 10,
52
+ maxMs: 5,
53
+ until: { $eq: [ref('value.x'), lit(1)] },
54
+ },
55
+ },
56
+ ],
57
+ };
58
+
59
+ const result = checkFlow(flow, actions(['read']));
60
+ const paths = result.problems.map((problem) => problem.path);
61
+
62
+ expect(result.ok).toBe(false);
63
+ expect(paths).toContain('steps[0].action');
64
+ expect(paths).toContain('steps[1].with.x');
65
+ expect(paths).toContain('steps[2].poll.everyMs');
66
+ expect(paths).toContain('steps[2].poll.maxMs');
67
+ });
68
+
69
+ it('item 只在 forEach 步骤里', () => {
70
+ const flow: FlowDefinition = {
71
+ id: 'bad',
72
+ title: '坏',
73
+ steps: [
74
+ { id: 'a', action: 'read', with: { x: ref('item.id') } },
75
+ {
76
+ id: 'b',
77
+ action: 'read',
78
+ forEach: { over: { $lit: [1] }, as: 'item', max: 2 },
79
+ with: { x: ref('item.id') },
80
+ },
81
+ ],
82
+ };
83
+
84
+ const paths = checkFlow(flow, actions(['read'])).problems.map((problem) => problem.path);
85
+ expect(paths).toContain('steps[0].with.x');
86
+ expect(paths).not.toContain('steps[1].with.x');
87
+ });
88
+
89
+ it('forEach 里可以 poll;maxMs=0 与 onTimeout=continue 合法', () => {
90
+ const flow: FlowDefinition = {
91
+ id: 'ok',
92
+ title: '合格',
93
+ steps: [
94
+ {
95
+ id: 'a',
96
+ action: 'read',
97
+ forEach: { over: { $lit: [1] }, as: 'item', max: 2 },
98
+ poll: {
99
+ everyMs: 100,
100
+ maxMs: 0,
101
+ onTimeout: 'continue',
102
+ until: { $eq: [ref('value.x'), ref('item.id')] },
103
+ },
104
+ },
105
+ ],
106
+ };
107
+
108
+ expect(checkFlow(flow, actions(['read']))).toEqual({ ok: true, problems: [] });
109
+ });
110
+
111
+ it('子流程也照查,路径带 steps[i].flow', () => {
112
+ const flow: FlowDefinition = {
113
+ id: 'outer',
114
+ title: '外层',
115
+ steps: [
116
+ {
117
+ id: 'merge',
118
+ title: '合并',
119
+ with: { env: ref('in.env') },
120
+ flow: {
121
+ id: 'merge',
122
+ title: '合并',
123
+ steps: [{ id: 'mr', action: 'missing' }],
124
+ },
125
+ },
126
+ ],
127
+ };
128
+
129
+ expect(checkFlow(flow, actions([])).problems.map((problem) => problem.path)).toContain(
130
+ 'steps[0].flow.steps[0].action',
131
+ );
132
+ });
133
+
134
+ it('stopWhen 可以引用任意步骤,引用不存在的步骤会报', () => {
135
+ const good: FlowDefinition = {
136
+ id: 'good',
137
+ title: '好',
138
+ stopWhen: { $exists: ref('steps.b.done') },
139
+ steps: [
140
+ { id: 'a', action: 'read' },
141
+ { id: 'b', action: 'read' },
142
+ ],
143
+ };
144
+ const bad: FlowDefinition = {
145
+ id: 'bad',
146
+ title: '坏',
147
+ stopWhen: { $exists: ref('steps.nope.done') },
148
+ steps: [{ id: 'a', action: 'read' }],
149
+ };
150
+
151
+ expect(checkFlow(good, actions(['read'])).ok).toBe(true);
152
+ expect(checkFlow(bad, actions(['read'])).problems.map((problem) => problem.path)).toContain(
153
+ 'stopWhen.$exists',
154
+ );
155
+ });
156
+
157
+ it('target 不能引用还没跑的步骤', () => {
158
+ const flow: FlowDefinition = {
159
+ id: 'bad',
160
+ title: '坏',
161
+ target: ref('steps.a.x'),
162
+ steps: [{ id: 'a', action: 'read' }],
163
+ };
164
+
165
+ expect(checkFlow(flow, actions(['read'])).problems.map((problem) => problem.path)).toContain(
166
+ 'target',
167
+ );
168
+ });
169
+ });