@nanobpm/nano-workforce 0.80.0 → 0.82.0
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.
- package/CHANGELOG.md +14 -0
- package/SPEC.md +11 -0
- package/app/contracts.ts +23 -0
- package/app/epicPhase.test.ts +62 -0
- package/app/epicPhase.ts +125 -0
- package/app/plan.ts +12 -0
- package/app/readiness.test.ts +300 -0
- package/app/readiness.ts +493 -0
- package/app/reviewWait.test.ts +19 -0
- package/app/reviewWait.ts +20 -0
- package/db/migrations/038_plan_epic_phase.sql +12 -0
- package/e2e/readiness-gate.e2e.ts +285 -0
- package/nano.app.json +4 -0
- package/package.json +1 -1
- package/pages/epic-detail.page.json +1 -0
- package/pages/epic.page.json +1 -0
- package/resources/forms/readiness-escalation.form +29 -0
- package/resources/processes/readiness-gate.bpmn +343 -0
- package/workers/readiness-probe/worker.test.ts +236 -0
- package/workers/readiness-probe/worker.ts +146 -0
- package/workers/record-plan/worker.ts +6 -0
- package/workers/record-results/worker.ts +8 -0
- package/workers/record-wave/worker.test.ts +5 -0
- package/workers/record-wave/worker.ts +12 -0
- package/workers/select-wave/worker.test.ts +4 -1
- package/workers/select-wave/worker.ts +9 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:nano="https://nanobpm.io/schema/shapes/1.0" id="Definitions_nano_workforce_readiness" targetNamespace="http://nanobpm.io/nano-workforce">
|
|
3
|
+
<bpmn:message id="Message_readinessReady" name="readiness-ready">
|
|
4
|
+
<bpmn:extensionElements>
|
|
5
|
+
<zeebe:subscription correlationKey="=gateKey" />
|
|
6
|
+
<zeebe:properties>
|
|
7
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="ReadinessReady" />
|
|
8
|
+
</zeebe:properties>
|
|
9
|
+
</bpmn:extensionElements>
|
|
10
|
+
</bpmn:message>
|
|
11
|
+
<bpmn:process id="readiness-gate" name="Artifact readiness wait-gate" isExecutable="true">
|
|
12
|
+
<bpmn:extensionElements>
|
|
13
|
+
<nano:shapes>
|
|
14
|
+
<nano:shape id="ReadinessProbeMatch" name="Readiness probe — match predicate">
|
|
15
|
+
<nano:extend name="status" type="integer" optional="true" />
|
|
16
|
+
<nano:extend name="bodyIncludes" type="string" optional="true" />
|
|
17
|
+
<nano:extend name="exitCode" type="integer" optional="true" />
|
|
18
|
+
<nano:extend name="stdoutIncludes" type="string" optional="true" />
|
|
19
|
+
<nano:extend name="version" type="string" optional="true" />
|
|
20
|
+
<nano:extend name="conclusion" type="string" optional="true" />
|
|
21
|
+
<nano:extend name="checkName" type="string" optional="true" />
|
|
22
|
+
</nano:shape>
|
|
23
|
+
<nano:shape id="ReadinessProbePoll" name="Readiness probe — poll policy">
|
|
24
|
+
<nano:extend name="everyMs" type="integer" optional="true" />
|
|
25
|
+
<nano:extend name="timeoutMs" type="integer" optional="true" />
|
|
26
|
+
<nano:extend name="backoff" type="string" optional="true" />
|
|
27
|
+
</nano:shape>
|
|
28
|
+
<nano:shape id="ReadinessProbe" name="Readiness probe descriptor">
|
|
29
|
+
<nano:extend name="kind" type="string" />
|
|
30
|
+
<nano:extend name="target" type="string" />
|
|
31
|
+
<nano:reference name="match" ref="ReadinessProbeMatch" />
|
|
32
|
+
<nano:reference name="poll" ref="ReadinessProbePoll" />
|
|
33
|
+
<nano:extend name="onTimeout" type="string" optional="true" />
|
|
34
|
+
<nano:extend name="credentialEnv" type="string" optional="true" />
|
|
35
|
+
</nano:shape>
|
|
36
|
+
<nano:shape id="ReadinessProbeIn" name="Readiness probe — input">
|
|
37
|
+
<nano:extend name="gateKey" type="string" />
|
|
38
|
+
<nano:extend name="probeTimeout" type="string" />
|
|
39
|
+
<nano:extend name="onTimeout" type="string" optional="true" />
|
|
40
|
+
<nano:reference name="probe" ref="ReadinessProbe" />
|
|
41
|
+
</nano:shape>
|
|
42
|
+
<nano:shape id="ReadinessProbeOut" name="Readiness probe — result">
|
|
43
|
+
<nano:extend name="ready" type="boolean" />
|
|
44
|
+
<nano:extend name="detail" type="string" optional="true" />
|
|
45
|
+
</nano:shape>
|
|
46
|
+
<nano:shape id="ReadinessReady" name="readiness-ready message payload">
|
|
47
|
+
<nano:extend name="ready" type="boolean" />
|
|
48
|
+
<nano:extend name="detail" type="string" optional="true" />
|
|
49
|
+
</nano:shape>
|
|
50
|
+
</nano:shapes>
|
|
51
|
+
</bpmn:extensionElements>
|
|
52
|
+
<bpmn:startEvent id="Start" name="Gate opened">
|
|
53
|
+
<bpmn:outgoing>f_start</bpmn:outgoing>
|
|
54
|
+
</bpmn:startEvent>
|
|
55
|
+
<bpmn:parallelGateway id="fork" name="arm probe + gate">
|
|
56
|
+
<bpmn:incoming>f_start</bpmn:incoming>
|
|
57
|
+
<bpmn:outgoing>f_toProbe</bpmn:outgoing>
|
|
58
|
+
<bpmn:outgoing>f_toGate</bpmn:outgoing>
|
|
59
|
+
</bpmn:parallelGateway>
|
|
60
|
+
<bpmn:serviceTask id="probe" name="Probe readiness (poll)">
|
|
61
|
+
<bpmn:extensionElements>
|
|
62
|
+
<zeebe:taskDefinition type="pr.readiness-probe" />
|
|
63
|
+
<zeebe:properties>
|
|
64
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="ReadinessProbeIn" />
|
|
65
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.out" value="ReadinessProbeOut" />
|
|
66
|
+
</zeebe:properties>
|
|
67
|
+
</bpmn:extensionElements>
|
|
68
|
+
<bpmn:incoming>f_toProbe</bpmn:incoming>
|
|
69
|
+
<bpmn:outgoing>f_probed</bpmn:outgoing>
|
|
70
|
+
</bpmn:serviceTask>
|
|
71
|
+
<bpmn:boundaryEvent id="probe-timeout" name="Gate timed out" attachedToRef="probe">
|
|
72
|
+
<bpmn:outgoing>f_probeTimeout</bpmn:outgoing>
|
|
73
|
+
<bpmn:timerEventDefinition id="ted_probeTimeout">
|
|
74
|
+
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">=probeTimeout</bpmn:timeDuration>
|
|
75
|
+
</bpmn:timerEventDefinition>
|
|
76
|
+
</bpmn:boundaryEvent>
|
|
77
|
+
<bpmn:endEvent id="probe-done" name="Probe settled">
|
|
78
|
+
<bpmn:incoming>f_probed</bpmn:incoming>
|
|
79
|
+
<bpmn:incoming>f_probeTimeout</bpmn:incoming>
|
|
80
|
+
</bpmn:endEvent>
|
|
81
|
+
<bpmn:eventBasedGateway id="gw-gate" name="ready or timeout?">
|
|
82
|
+
<bpmn:incoming>f_toGate</bpmn:incoming>
|
|
83
|
+
<bpmn:outgoing>f_toReady</bpmn:outgoing>
|
|
84
|
+
<bpmn:outgoing>f_toTimeout</bpmn:outgoing>
|
|
85
|
+
</bpmn:eventBasedGateway>
|
|
86
|
+
<bpmn:intermediateCatchEvent id="wait-ready" name="Wait: readiness signal">
|
|
87
|
+
<bpmn:incoming>f_toReady</bpmn:incoming>
|
|
88
|
+
<bpmn:outgoing>f_ready</bpmn:outgoing>
|
|
89
|
+
<bpmn:messageEventDefinition id="med_readinessReady" messageRef="Message_readinessReady" />
|
|
90
|
+
</bpmn:intermediateCatchEvent>
|
|
91
|
+
<bpmn:intermediateCatchEvent id="wait-timeout" name="Wait: readiness timeout">
|
|
92
|
+
<bpmn:incoming>f_toTimeout</bpmn:incoming>
|
|
93
|
+
<bpmn:outgoing>f_timedOut</bpmn:outgoing>
|
|
94
|
+
<bpmn:timerEventDefinition id="ted_readinessTimeout">
|
|
95
|
+
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">=probeTimeout</bpmn:timeDuration>
|
|
96
|
+
</bpmn:timerEventDefinition>
|
|
97
|
+
</bpmn:intermediateCatchEvent>
|
|
98
|
+
<bpmn:endEvent id="gate-ready" name="Ready">
|
|
99
|
+
<bpmn:extensionElements>
|
|
100
|
+
<zeebe:ioMapping>
|
|
101
|
+
<zeebe:input source="="ready"" target="gateOutcome" />
|
|
102
|
+
</zeebe:ioMapping>
|
|
103
|
+
</bpmn:extensionElements>
|
|
104
|
+
<bpmn:incoming>f_ready</bpmn:incoming>
|
|
105
|
+
</bpmn:endEvent>
|
|
106
|
+
<bpmn:exclusiveGateway id="gw-onTimeout" name="on timeout?" default="f_escalate">
|
|
107
|
+
<bpmn:incoming>f_timedOut</bpmn:incoming>
|
|
108
|
+
<bpmn:outgoing>f_escalate</bpmn:outgoing>
|
|
109
|
+
<bpmn:outgoing>f_fail</bpmn:outgoing>
|
|
110
|
+
<bpmn:outgoing>f_continue</bpmn:outgoing>
|
|
111
|
+
</bpmn:exclusiveGateway>
|
|
112
|
+
<bpmn:userTask id="readiness-escalation" name="Readiness escalation (human)">
|
|
113
|
+
<bpmn:extensionElements>
|
|
114
|
+
<zeebe:formDefinition formId="readiness-escalation" />
|
|
115
|
+
<zeebe:userTask />
|
|
116
|
+
<zeebe:assignmentDefinition candidateGroups="operators" />
|
|
117
|
+
</bpmn:extensionElements>
|
|
118
|
+
<bpmn:incoming>f_escalate</bpmn:incoming>
|
|
119
|
+
<bpmn:outgoing>f_toResolution</bpmn:outgoing>
|
|
120
|
+
</bpmn:userTask>
|
|
121
|
+
<bpmn:exclusiveGateway id="gw-resolution" name="resolution?" default="f_ack">
|
|
122
|
+
<bpmn:incoming>f_toResolution</bpmn:incoming>
|
|
123
|
+
<bpmn:outgoing>f_ack</bpmn:outgoing>
|
|
124
|
+
<bpmn:outgoing>f_abandon</bpmn:outgoing>
|
|
125
|
+
</bpmn:exclusiveGateway>
|
|
126
|
+
<bpmn:endEvent id="gate-escalated" name="Escalated">
|
|
127
|
+
<bpmn:extensionElements>
|
|
128
|
+
<zeebe:ioMapping>
|
|
129
|
+
<zeebe:input source="="escalated"" target="gateOutcome" />
|
|
130
|
+
</zeebe:ioMapping>
|
|
131
|
+
</bpmn:extensionElements>
|
|
132
|
+
<bpmn:incoming>f_ack</bpmn:incoming>
|
|
133
|
+
</bpmn:endEvent>
|
|
134
|
+
<bpmn:endEvent id="gate-failed" name="Failed">
|
|
135
|
+
<bpmn:extensionElements>
|
|
136
|
+
<zeebe:ioMapping>
|
|
137
|
+
<zeebe:input source="="failed"" target="gateOutcome" />
|
|
138
|
+
</zeebe:ioMapping>
|
|
139
|
+
</bpmn:extensionElements>
|
|
140
|
+
<bpmn:incoming>f_fail</bpmn:incoming>
|
|
141
|
+
<bpmn:incoming>f_abandon</bpmn:incoming>
|
|
142
|
+
</bpmn:endEvent>
|
|
143
|
+
<bpmn:endEvent id="gate-continued" name="Continued">
|
|
144
|
+
<bpmn:extensionElements>
|
|
145
|
+
<zeebe:ioMapping>
|
|
146
|
+
<zeebe:input source="="continued"" target="gateOutcome" />
|
|
147
|
+
</zeebe:ioMapping>
|
|
148
|
+
</bpmn:extensionElements>
|
|
149
|
+
<bpmn:incoming>f_continue</bpmn:incoming>
|
|
150
|
+
</bpmn:endEvent>
|
|
151
|
+
<bpmn:sequenceFlow id="f_start" sourceRef="Start" targetRef="fork" />
|
|
152
|
+
<bpmn:sequenceFlow id="f_toProbe" sourceRef="fork" targetRef="probe" />
|
|
153
|
+
<bpmn:sequenceFlow id="f_toGate" sourceRef="fork" targetRef="gw-gate" />
|
|
154
|
+
<bpmn:sequenceFlow id="f_probed" sourceRef="probe" targetRef="probe-done" />
|
|
155
|
+
<bpmn:sequenceFlow id="f_probeTimeout" sourceRef="probe-timeout" targetRef="probe-done" />
|
|
156
|
+
<bpmn:sequenceFlow id="f_toReady" sourceRef="gw-gate" targetRef="wait-ready" />
|
|
157
|
+
<bpmn:sequenceFlow id="f_toTimeout" sourceRef="gw-gate" targetRef="wait-timeout" />
|
|
158
|
+
<bpmn:sequenceFlow id="f_ready" sourceRef="wait-ready" targetRef="gate-ready" />
|
|
159
|
+
<bpmn:sequenceFlow id="f_timedOut" sourceRef="wait-timeout" targetRef="gw-onTimeout" />
|
|
160
|
+
<bpmn:sequenceFlow id="f_escalate" name="escalate" sourceRef="gw-onTimeout" targetRef="readiness-escalation" />
|
|
161
|
+
<bpmn:sequenceFlow id="f_fail" name="fail" sourceRef="gw-onTimeout" targetRef="gate-failed">
|
|
162
|
+
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=(if probe != null and probe.onTimeout != null then probe.onTimeout else onTimeout) = "fail"</bpmn:conditionExpression>
|
|
163
|
+
</bpmn:sequenceFlow>
|
|
164
|
+
<bpmn:sequenceFlow id="f_continue" name="continue" sourceRef="gw-onTimeout" targetRef="gate-continued">
|
|
165
|
+
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=(if probe != null and probe.onTimeout != null then probe.onTimeout else onTimeout) = "continue"</bpmn:conditionExpression>
|
|
166
|
+
</bpmn:sequenceFlow>
|
|
167
|
+
<bpmn:sequenceFlow id="f_toResolution" sourceRef="readiness-escalation" targetRef="gw-resolution" />
|
|
168
|
+
<bpmn:sequenceFlow id="f_ack" name="acknowledge" sourceRef="gw-resolution" targetRef="gate-escalated" />
|
|
169
|
+
<bpmn:sequenceFlow id="f_abandon" name="abandon" sourceRef="gw-resolution" targetRef="gate-failed">
|
|
170
|
+
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=resolution = "abandon"</bpmn:conditionExpression>
|
|
171
|
+
</bpmn:sequenceFlow>
|
|
172
|
+
</bpmn:process>
|
|
173
|
+
<bpmndi:BPMNDiagram id="BPMNDiagram_readiness-gate">
|
|
174
|
+
<bpmndi:BPMNPlane id="BPMNPlane_readiness-gate" bpmnElement="readiness-gate">
|
|
175
|
+
<bpmndi:BPMNShape id="BPMNShape_Start" bpmnElement="Start">
|
|
176
|
+
<dc:Bounds x="80" y="102" width="36" height="36" />
|
|
177
|
+
<bpmndi:BPMNLabel>
|
|
178
|
+
<dc:Bounds x="58" y="143" width="80" height="14" />
|
|
179
|
+
</bpmndi:BPMNLabel>
|
|
180
|
+
</bpmndi:BPMNShape>
|
|
181
|
+
<bpmndi:BPMNShape id="BPMNShape_fork" bpmnElement="fork" isMarkerVisible="true">
|
|
182
|
+
<dc:Bounds x="216" y="95" width="50" height="50" />
|
|
183
|
+
<bpmndi:BPMNLabel>
|
|
184
|
+
<dc:Bounds x="202" y="62" width="78" height="28" />
|
|
185
|
+
</bpmndi:BPMNLabel>
|
|
186
|
+
</bpmndi:BPMNShape>
|
|
187
|
+
<bpmndi:BPMNShape id="BPMNShape_probe" bpmnElement="probe">
|
|
188
|
+
<dc:Bounds x="1288" y="80" width="100" height="80" />
|
|
189
|
+
</bpmndi:BPMNShape>
|
|
190
|
+
<bpmndi:BPMNShape id="BPMNShape_probe-done" bpmnElement="probe-done">
|
|
191
|
+
<dc:Bounds x="1488" y="102" width="36" height="36" />
|
|
192
|
+
<bpmndi:BPMNLabel>
|
|
193
|
+
<dc:Bounds x="1480" y="69" width="53" height="28" />
|
|
194
|
+
</bpmndi:BPMNLabel>
|
|
195
|
+
</bpmndi:BPMNShape>
|
|
196
|
+
<bpmndi:BPMNShape id="BPMNShape_gw-gate" bpmnElement="gw-gate" isMarkerVisible="true">
|
|
197
|
+
<dc:Bounds x="366" y="255" width="50" height="50" />
|
|
198
|
+
<bpmndi:BPMNLabel>
|
|
199
|
+
<dc:Bounds x="360" y="222" width="63" height="28" />
|
|
200
|
+
</bpmndi:BPMNLabel>
|
|
201
|
+
</bpmndi:BPMNShape>
|
|
202
|
+
<bpmndi:BPMNShape id="BPMNShape_wait-ready" bpmnElement="wait-ready">
|
|
203
|
+
<dc:Bounds x="516" y="262" width="36" height="36" />
|
|
204
|
+
<bpmndi:BPMNLabel>
|
|
205
|
+
<dc:Bounds x="501" y="303" width="67" height="42" />
|
|
206
|
+
</bpmndi:BPMNLabel>
|
|
207
|
+
</bpmndi:BPMNShape>
|
|
208
|
+
<bpmndi:BPMNShape id="BPMNShape_wait-timeout" bpmnElement="wait-timeout">
|
|
209
|
+
<dc:Bounds x="516" y="422" width="36" height="36" />
|
|
210
|
+
<bpmndi:BPMNLabel>
|
|
211
|
+
<dc:Bounds x="501" y="463" width="67" height="42" />
|
|
212
|
+
</bpmndi:BPMNLabel>
|
|
213
|
+
</bpmndi:BPMNShape>
|
|
214
|
+
<bpmndi:BPMNShape id="BPMNShape_gate-ready" bpmnElement="gate-ready">
|
|
215
|
+
<dc:Bounds x="659" y="262" width="36" height="36" />
|
|
216
|
+
<bpmndi:BPMNLabel>
|
|
217
|
+
<dc:Bounds x="657" y="303" width="41" height="14" />
|
|
218
|
+
</bpmndi:BPMNLabel>
|
|
219
|
+
</bpmndi:BPMNShape>
|
|
220
|
+
<bpmndi:BPMNShape id="BPMNShape_gw-onTimeout" bpmnElement="gw-onTimeout" isMarkerVisible="true">
|
|
221
|
+
<dc:Bounds x="652" y="415" width="50" height="50" />
|
|
222
|
+
<bpmndi:BPMNLabel>
|
|
223
|
+
<dc:Bounds x="637" y="396" width="81" height="14" />
|
|
224
|
+
</bpmndi:BPMNLabel>
|
|
225
|
+
</bpmndi:BPMNShape>
|
|
226
|
+
<bpmndi:BPMNShape id="BPMNShape_readiness-escalation" bpmnElement="readiness-escalation">
|
|
227
|
+
<dc:Bounds x="802" y="400" width="100" height="80" />
|
|
228
|
+
</bpmndi:BPMNShape>
|
|
229
|
+
<bpmndi:BPMNShape id="BPMNShape_gw-resolution" bpmnElement="gw-resolution" isMarkerVisible="true">
|
|
230
|
+
<dc:Bounds x="1002" y="415" width="50" height="50" />
|
|
231
|
+
<bpmndi:BPMNLabel>
|
|
232
|
+
<dc:Bounds x="987" y="396" width="81" height="14" />
|
|
233
|
+
</bpmndi:BPMNLabel>
|
|
234
|
+
</bpmndi:BPMNShape>
|
|
235
|
+
<bpmndi:BPMNShape id="BPMNShape_gate-escalated" bpmnElement="gate-escalated">
|
|
236
|
+
<dc:Bounds x="1152" y="422" width="36" height="36" />
|
|
237
|
+
<bpmndi:BPMNLabel>
|
|
238
|
+
<dc:Bounds x="1136" y="403" width="69" height="14" />
|
|
239
|
+
</bpmndi:BPMNLabel>
|
|
240
|
+
</bpmndi:BPMNShape>
|
|
241
|
+
<bpmndi:BPMNShape id="BPMNShape_gate-failed" bpmnElement="gate-failed">
|
|
242
|
+
<dc:Bounds x="1152" y="582" width="36" height="36" />
|
|
243
|
+
<bpmndi:BPMNLabel>
|
|
244
|
+
<dc:Bounds x="1146" y="623" width="48" height="14" />
|
|
245
|
+
</bpmndi:BPMNLabel>
|
|
246
|
+
</bpmndi:BPMNShape>
|
|
247
|
+
<bpmndi:BPMNShape id="BPMNShape_gate-continued" bpmnElement="gate-continued">
|
|
248
|
+
<dc:Bounds x="834" y="742" width="36" height="36" />
|
|
249
|
+
<bpmndi:BPMNLabel>
|
|
250
|
+
<dc:Bounds x="818" y="783" width="69" height="14" />
|
|
251
|
+
</bpmndi:BPMNLabel>
|
|
252
|
+
</bpmndi:BPMNShape>
|
|
253
|
+
<bpmndi:BPMNShape id="BPMNShape_probe-timeout" bpmnElement="probe-timeout">
|
|
254
|
+
<dc:Bounds x="1320" y="142" width="36" height="36" />
|
|
255
|
+
<bpmndi:BPMNLabel>
|
|
256
|
+
<dc:Bounds x="1300" y="203" width="76" height="28" />
|
|
257
|
+
</bpmndi:BPMNLabel>
|
|
258
|
+
</bpmndi:BPMNShape>
|
|
259
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_start" bpmnElement="f_start">
|
|
260
|
+
<di:waypoint x="116" y="120" />
|
|
261
|
+
<di:waypoint x="216" y="120" />
|
|
262
|
+
</bpmndi:BPMNEdge>
|
|
263
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_toProbe" bpmnElement="f_toProbe">
|
|
264
|
+
<di:waypoint x="266" y="120" />
|
|
265
|
+
<di:waypoint x="1288" y="120" />
|
|
266
|
+
</bpmndi:BPMNEdge>
|
|
267
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_probed" bpmnElement="f_probed">
|
|
268
|
+
<di:waypoint x="1388" y="120" />
|
|
269
|
+
<di:waypoint x="1488" y="120" />
|
|
270
|
+
</bpmndi:BPMNEdge>
|
|
271
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_toReady" bpmnElement="f_toReady">
|
|
272
|
+
<di:waypoint x="416" y="280" />
|
|
273
|
+
<di:waypoint x="516" y="280" />
|
|
274
|
+
</bpmndi:BPMNEdge>
|
|
275
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_ready" bpmnElement="f_ready">
|
|
276
|
+
<di:waypoint x="552" y="280" />
|
|
277
|
+
<di:waypoint x="659" y="280" />
|
|
278
|
+
</bpmndi:BPMNEdge>
|
|
279
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_escalate" bpmnElement="f_escalate">
|
|
280
|
+
<di:waypoint x="702" y="440" />
|
|
281
|
+
<di:waypoint x="802" y="440" />
|
|
282
|
+
<bpmndi:BPMNLabel>
|
|
283
|
+
<dc:Bounds x="722" y="448" width="60" height="14" />
|
|
284
|
+
</bpmndi:BPMNLabel>
|
|
285
|
+
</bpmndi:BPMNEdge>
|
|
286
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_toResolution" bpmnElement="f_toResolution">
|
|
287
|
+
<di:waypoint x="902" y="440" />
|
|
288
|
+
<di:waypoint x="1002" y="440" />
|
|
289
|
+
</bpmndi:BPMNEdge>
|
|
290
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_ack" bpmnElement="f_ack">
|
|
291
|
+
<di:waypoint x="1052" y="440" />
|
|
292
|
+
<di:waypoint x="1152" y="440" />
|
|
293
|
+
<bpmndi:BPMNLabel>
|
|
294
|
+
<dc:Bounds x="1060" y="448" width="84" height="14" />
|
|
295
|
+
</bpmndi:BPMNLabel>
|
|
296
|
+
</bpmndi:BPMNEdge>
|
|
297
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_toGate" bpmnElement="f_toGate">
|
|
298
|
+
<di:waypoint x="241" y="145" />
|
|
299
|
+
<di:waypoint x="241" y="280" />
|
|
300
|
+
<di:waypoint x="366" y="280" />
|
|
301
|
+
</bpmndi:BPMNEdge>
|
|
302
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_toTimeout" bpmnElement="f_toTimeout">
|
|
303
|
+
<di:waypoint x="391" y="305" />
|
|
304
|
+
<di:waypoint x="391" y="440" />
|
|
305
|
+
<di:waypoint x="516" y="440" />
|
|
306
|
+
</bpmndi:BPMNEdge>
|
|
307
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_fail" bpmnElement="f_fail">
|
|
308
|
+
<di:waypoint x="677" y="465" />
|
|
309
|
+
<di:waypoint x="677" y="600" />
|
|
310
|
+
<di:waypoint x="1152" y="600" />
|
|
311
|
+
<bpmndi:BPMNLabel>
|
|
312
|
+
<dc:Bounds x="836" y="578" width="32" height="14" />
|
|
313
|
+
</bpmndi:BPMNLabel>
|
|
314
|
+
</bpmndi:BPMNEdge>
|
|
315
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_continue" bpmnElement="f_continue">
|
|
316
|
+
<di:waypoint x="677" y="465" />
|
|
317
|
+
<di:waypoint x="677" y="760" />
|
|
318
|
+
<di:waypoint x="834" y="760" />
|
|
319
|
+
<bpmndi:BPMNLabel>
|
|
320
|
+
<dc:Bounds x="682" y="673" width="60" height="14" />
|
|
321
|
+
</bpmndi:BPMNLabel>
|
|
322
|
+
</bpmndi:BPMNEdge>
|
|
323
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_abandon" bpmnElement="f_abandon">
|
|
324
|
+
<di:waypoint x="1027" y="465" />
|
|
325
|
+
<di:waypoint x="1027" y="600" />
|
|
326
|
+
<di:waypoint x="1152" y="600" />
|
|
327
|
+
<bpmndi:BPMNLabel>
|
|
328
|
+
<dc:Bounds x="1032" y="526" width="53" height="14" />
|
|
329
|
+
</bpmndi:BPMNLabel>
|
|
330
|
+
</bpmndi:BPMNEdge>
|
|
331
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_timedOut" bpmnElement="f_timedOut">
|
|
332
|
+
<di:waypoint x="552" y="440" />
|
|
333
|
+
<di:waypoint x="652" y="440" />
|
|
334
|
+
</bpmndi:BPMNEdge>
|
|
335
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_probeTimeout" bpmnElement="f_probeTimeout">
|
|
336
|
+
<di:waypoint x="1338" y="178" />
|
|
337
|
+
<di:waypoint x="1338" y="198" />
|
|
338
|
+
<di:waypoint x="1506" y="198" />
|
|
339
|
+
<di:waypoint x="1506" y="138" />
|
|
340
|
+
</bpmndi:BPMNEdge>
|
|
341
|
+
</bpmndi:BPMNPlane>
|
|
342
|
+
</bpmndi:BPMNDiagram>
|
|
343
|
+
</bpmn:definitions>
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// Unit coverage for the pr.readiness-probe poll loop (workers/readiness-probe/worker.ts, #258).
|
|
2
|
+
//
|
|
3
|
+
// The loop is factored out (`pollUntilReady`) with injectable I/O, clock, and publisher so the two
|
|
4
|
+
// load-bearing behaviours are proven without a network, a subprocess, or a real timer:
|
|
5
|
+
// • ready → it publishes the `readiness-ready` message exactly once and returns ready;
|
|
6
|
+
// • never-ready → it EXHAUSTS its local budget and returns not-ready WITHOUT publishing (the
|
|
7
|
+
// engine timer, not the worker, is the authoritative bound) — and it must not loop forever.
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import { assert, assertEquals, assertRejects } from "#test-assert";
|
|
10
|
+
import type { CommandResult, HttpResponse, ProbeExec, ReadinessProbe } from "../../app/readiness.ts";
|
|
11
|
+
import { parseProbe } from "../../app/readiness.ts";
|
|
12
|
+
import handler, { pollUntilReady, READINESS_READY_MESSAGE, readGateVars } from "./worker.ts";
|
|
13
|
+
|
|
14
|
+
// A virtual clock: `now()` advances only when the loop's `wait(ms)` is called, so a never-ready
|
|
15
|
+
// probe races to its deadline in zero real time (no setTimeout) and the test can never hang.
|
|
16
|
+
function fakeClock() {
|
|
17
|
+
let t = 0;
|
|
18
|
+
return {
|
|
19
|
+
now: () => t,
|
|
20
|
+
wait: async (ms: number) => {
|
|
21
|
+
t += ms;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function execReturning(seq: Array<HttpResponse>): ProbeExec {
|
|
27
|
+
let i = 0;
|
|
28
|
+
return {
|
|
29
|
+
async httpGet() {
|
|
30
|
+
const r = seq[Math.min(i, seq.length - 1)];
|
|
31
|
+
i += 1;
|
|
32
|
+
return r;
|
|
33
|
+
},
|
|
34
|
+
async run(): Promise<CommandResult> {
|
|
35
|
+
return { code: 1, stdout: "", stderr: "" };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const httpProbe = (poll: ReadinessProbe["poll"]): ReadinessProbe =>
|
|
41
|
+
parseProbe({ kind: "http", target: "https://x/health", poll });
|
|
42
|
+
|
|
43
|
+
test("pollUntilReady: publishes readiness-ready once and returns ready when a probe goes green", async () => {
|
|
44
|
+
const clock = fakeClock();
|
|
45
|
+
const published: Array<{ detail: string }> = [];
|
|
46
|
+
const exec = execReturning([{ status: 503, body: "" }, { status: 200, body: "ok" }]);
|
|
47
|
+
const res = await pollUntilReady({
|
|
48
|
+
probe: httpProbe({ everyMs: 10, timeoutMs: 10_000, backoff: "fixed" }),
|
|
49
|
+
gateKey: "gate-1",
|
|
50
|
+
exec,
|
|
51
|
+
env: {},
|
|
52
|
+
now: clock.now,
|
|
53
|
+
wait: clock.wait,
|
|
54
|
+
publish: async (detail) => {
|
|
55
|
+
published.push({ detail });
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
assert(res.ready, "the probe eventually reported ready");
|
|
59
|
+
assertEquals(published.length, 1, "exactly one readiness message was published");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("pollUntilReady: a never-green probe exhausts its budget and returns not-ready WITHOUT publishing", async () => {
|
|
63
|
+
const clock = fakeClock();
|
|
64
|
+
let publishes = 0;
|
|
65
|
+
const exec = execReturning([{ status: 503, body: "" }]);
|
|
66
|
+
const res = await pollUntilReady({
|
|
67
|
+
probe: httpProbe({ everyMs: 5, timeoutMs: 100, backoff: "fixed" }),
|
|
68
|
+
gateKey: "gate-2",
|
|
69
|
+
exec,
|
|
70
|
+
env: {},
|
|
71
|
+
now: clock.now,
|
|
72
|
+
wait: clock.wait,
|
|
73
|
+
publish: async () => {
|
|
74
|
+
publishes += 1;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
assert(!res.ready, "the wait was bounded — the loop gave up instead of hanging");
|
|
78
|
+
assertEquals(publishes, 0, "a not-ready probe never publishes a readiness signal");
|
|
79
|
+
assert(clock.now() <= 100, "the loop stopped at (or before) its declared budget");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("pollUntilReady: an I/O throw is caught and treated as not-ready (never rejects), and its raw message is not leaked", async () => {
|
|
83
|
+
const clock = fakeClock();
|
|
84
|
+
const seen: string[] = [];
|
|
85
|
+
const exec: ProbeExec = {
|
|
86
|
+
async httpGet() {
|
|
87
|
+
throw new Error("connection refused to https://h/p?token=s3cr3t");
|
|
88
|
+
},
|
|
89
|
+
async run(): Promise<CommandResult> {
|
|
90
|
+
return { code: 1, stdout: "", stderr: "" };
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const res = await pollUntilReady({
|
|
94
|
+
probe: httpProbe({ everyMs: 5, timeoutMs: 30, backoff: "fixed" }),
|
|
95
|
+
gateKey: "gate-3",
|
|
96
|
+
exec,
|
|
97
|
+
env: {},
|
|
98
|
+
now: clock.now,
|
|
99
|
+
wait: clock.wait,
|
|
100
|
+
publish: async () => {},
|
|
101
|
+
log: (msg) => seen.push(msg),
|
|
102
|
+
});
|
|
103
|
+
assert(!res.ready, "a transport failure is a transient not-ready, not a crash");
|
|
104
|
+
const all = [res.detail, ...seen].join("\n");
|
|
105
|
+
assert(!all.includes("s3cr3t"), "the raw error message (with its secret) must not leak");
|
|
106
|
+
assert(!all.includes("connection refused"), "only the error class is surfaced, not the message");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("handler: a blank gateKey fails fast (an empty correlationKey would never release the gate)", async () => {
|
|
110
|
+
const job = { variables: { probe: { kind: "http", target: "https://x/health" }, gateKey: " " } };
|
|
111
|
+
await assertRejects(
|
|
112
|
+
// biome-ignore lint/suspicious/noExplicitAny: minimal job/app stub for the fail-fast guard.
|
|
113
|
+
() => handler(job as any, {} as any),
|
|
114
|
+
Error,
|
|
115
|
+
"gateKey",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("pollUntilReady: the poll cadence reads NANO_READINESS_POLL_EVERY_MS from the injected env, not ambient process.env", async () => {
|
|
120
|
+
const clock = fakeClock();
|
|
121
|
+
// Descriptor omits everyMs, so the cadence falls back to the env contract. A small injected value
|
|
122
|
+
// (25ms) makes the loop step through its 100ms budget; the ambient default (15_000ms) would blow
|
|
123
|
+
// the budget on the first wait and bail at now()=0. Asserting the clock advanced proves the
|
|
124
|
+
// injected env — not process.env — drove the cadence.
|
|
125
|
+
const exec = execReturning([{ status: 503, body: "" }]);
|
|
126
|
+
const res = await pollUntilReady({
|
|
127
|
+
probe: httpProbe({ timeoutMs: 100, backoff: "fixed" }),
|
|
128
|
+
gateKey: "gate-env",
|
|
129
|
+
exec,
|
|
130
|
+
env: { NANO_READINESS_POLL_EVERY_MS: "25" },
|
|
131
|
+
now: clock.now,
|
|
132
|
+
wait: clock.wait,
|
|
133
|
+
publish: async () => {},
|
|
134
|
+
});
|
|
135
|
+
assert(!res.ready, "the never-green probe exhausted its budget");
|
|
136
|
+
assert(clock.now() > 0, "the injected env's 25ms cadence stepped the clock (ambient default would bail at 0)");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("pollUntilReady: an omitted poll.timeoutMs takes the budget from NANO_READINESS_POLL_TIMEOUT, not the built-in 30m", async () => {
|
|
140
|
+
const clock = fakeClock();
|
|
141
|
+
// Regression for the worker/gate timeout drift: the gate's engine timer derives from
|
|
142
|
+
// NANO_READINESS_POLL_TIMEOUT when the descriptor omits poll.timeoutMs, but the worker used to
|
|
143
|
+
// fall back to the hard-coded 30m default. With a 45m env budget and no descriptor timeout, the
|
|
144
|
+
// never-green probe must keep polling PAST 30m (up to the 45m the gate itself waits) so it can't
|
|
145
|
+
// go silent while the gate is still waiting. everyMs is clamped to MAX_EVERY_MS (5m), so ~9 waits
|
|
146
|
+
// land the clock beyond 30m only if the env budget — not the 30m default — is in force.
|
|
147
|
+
const exec = execReturning([{ status: 503, body: "" }]);
|
|
148
|
+
const THIRTY_MIN = 30 * 60_000;
|
|
149
|
+
const res = await pollUntilReady({
|
|
150
|
+
probe: httpProbe({ everyMs: 5 * 60_000, backoff: "fixed" }),
|
|
151
|
+
gateKey: "gate-timeout-env",
|
|
152
|
+
exec,
|
|
153
|
+
env: { NANO_READINESS_POLL_TIMEOUT: "PT45M" },
|
|
154
|
+
now: clock.now,
|
|
155
|
+
wait: clock.wait,
|
|
156
|
+
publish: async () => {},
|
|
157
|
+
});
|
|
158
|
+
assert(!res.ready, "the never-green probe exhausted its budget");
|
|
159
|
+
assert(
|
|
160
|
+
clock.now() > THIRTY_MIN,
|
|
161
|
+
`the worker honored the 45m env budget (probed past 30m); stopped at ${clock.now()}ms`,
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("pollUntilReady: the seeded probeTimeout binds the worker to the gate per-instance, overriding ambient env", async () => {
|
|
166
|
+
const clock = fakeClock();
|
|
167
|
+
// Regression for the per-instance worker/gate drift: the gate's engine timers fire off the seeded
|
|
168
|
+
// `probeTimeout` process variable, so the worker must adopt THAT value, not recompute from the
|
|
169
|
+
// ambient env. Here the seeded bound (45m) is generous while the ambient env (1m) is stale/smaller.
|
|
170
|
+
// The env-recomputing code would bail after 1m — going silent while the engine still waits 45m —
|
|
171
|
+
// so the never-green probe must instead keep polling PAST 30m to prove the seeded value is in force.
|
|
172
|
+
const exec = execReturning([{ status: 503, body: "" }]);
|
|
173
|
+
const THIRTY_MIN = 30 * 60_000;
|
|
174
|
+
const res = await pollUntilReady({
|
|
175
|
+
probe: httpProbe({ everyMs: 5 * 60_000, backoff: "fixed" }),
|
|
176
|
+
gateKey: "gate-seeded",
|
|
177
|
+
probeTimeout: "PT45M",
|
|
178
|
+
exec,
|
|
179
|
+
env: { NANO_READINESS_POLL_TIMEOUT: "PT1M" },
|
|
180
|
+
now: clock.now,
|
|
181
|
+
wait: clock.wait,
|
|
182
|
+
publish: async () => {},
|
|
183
|
+
});
|
|
184
|
+
assert(!res.ready, "the never-green probe exhausted its (seeded) budget");
|
|
185
|
+
assert(
|
|
186
|
+
clock.now() > THIRTY_MIN,
|
|
187
|
+
`the worker honored the seeded 45m probeTimeout over the 1m env; stopped at ${clock.now()}ms`,
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("READINESS_READY_MESSAGE is the name the gate correlates", () => {
|
|
192
|
+
assertEquals(READINESS_READY_MESSAGE, "readiness-ready");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("readGateVars: returns the RAW (untrimmed) gateKey so the publish key matches the gate's =gateKey subscription", () => {
|
|
196
|
+
// The gate's message subscription binds correlationKey="=gateKey" (the untrimmed process variable).
|
|
197
|
+
// If a caller seeds a whitespace-padded gateKey, trimming the publish key would desync it from the
|
|
198
|
+
// subscription — a green probe would then release the wait only via the timeout arm. So the key we
|
|
199
|
+
// publish on must be the raw value, byte-for-byte.
|
|
200
|
+
const { gateKey } = readGateVars({ gateKey: " gate-x ", probeTimeout: "PT30M" });
|
|
201
|
+
assertEquals(gateKey, " gate-x ", "the raw gateKey is preserved for byte-for-byte correlation");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("readGateVars: a blank/whitespace gateKey fails fast (an empty correlationKey can never release the gate)", () => {
|
|
205
|
+
for (const gateKey of ["", " "]) {
|
|
206
|
+
let threw = false;
|
|
207
|
+
try {
|
|
208
|
+
readGateVars({ gateKey, probeTimeout: "PT30M" });
|
|
209
|
+
} catch (err) {
|
|
210
|
+
threw = true;
|
|
211
|
+
assert((err as Error).message.includes("gateKey"), "the error names the offending gateKey");
|
|
212
|
+
}
|
|
213
|
+
assert(threw, `a ${JSON.stringify(gateKey)} gateKey must fail fast`);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("readGateVars: a missing/blank probeTimeout fails fast rather than silently falling back to env", () => {
|
|
218
|
+
// probeTimeout drives BOTH the gate's engine timers (=probeTimeout). A missing/non-string value used
|
|
219
|
+
// to fall back to the env-derived twin, breaking the per-instance bound and masking a mis-seeded
|
|
220
|
+
// instance until it escalated. It must now fail fast.
|
|
221
|
+
for (const probeTimeout of [undefined, "", " ", 42, null]) {
|
|
222
|
+
let threw = false;
|
|
223
|
+
try {
|
|
224
|
+
readGateVars({ gateKey: "gate-1", probeTimeout });
|
|
225
|
+
} catch (err) {
|
|
226
|
+
threw = true;
|
|
227
|
+
assert((err as Error).message.includes("probeTimeout"), "the error names the offending probeTimeout");
|
|
228
|
+
}
|
|
229
|
+
assert(threw, `a ${JSON.stringify(probeTimeout)} probeTimeout must fail fast`);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("readGateVars: passes a valid probeTimeout through raw (the engine timer evaluates the same string)", () => {
|
|
234
|
+
const { probeTimeout } = readGateVars({ gateKey: "gate-1", probeTimeout: "PT45M" });
|
|
235
|
+
assertEquals(probeTimeout, "PT45M", "the seeded probeTimeout string flows through unchanged");
|
|
236
|
+
});
|