Commit 14bd30f
authored
fix(flow): resume reads the live subject, routed items reach their branch, portal rows carry case context, and one bad flow cannot silence an event (#3310)
* fix(flow): a resumed run reads the subject as it stands, not the trigger-time snapshot
A run's items begin as a snapshot of the subject, and resume replayed that
snapshot verbatim: the applicant supplied the missing description and
completed the task, and the re-check still read description: null off the
frozen item — the shipped re-ask loop could never succeed and the case
stranded. The live subject was already resolved on every resume path and
then used only as the marking holder.
Now both resume paths (worker execute() and the sync advanceStream())
refresh the subject's own fields on every stored item that IS the subject,
matched by identity, on the flat items list and the per-place buffers
alike. Live wins on the subject's keys; step-produced keys survive, so
flow-runs REQ-FR-003 (items survive the pause) still holds — the refresh
never re-seeds and never touches an item carrying another object's
identity.
Regression: FlowRunServiceTest::testAResumedRunBranchesOnTheLiveSubjectNotTheTriggerTimeSnapshot
and ::testAReAskLoopThatReceivesTheAnswerTerminates — both red before this
change (the loop one strands at the transition ceiling, the field symptom).
* fix(flow): a routing tag names an exit; resolve it to the exit's place before delivery
The two halves of per-item routing spoke different vocabularies. The route
node tags each item with the output its rule matched — an EXIT id ('no') —
while FlowItemPlacement::itemsForOutput() matches tags against PLACE names
('onRejected'). Unresolved, every routed item was discarded as the token
landed: the branch fired with zero items, SetFields iterated over nothing,
and the run persisted items: [], losing the seed and the earlier steps'
output. That is the sync task-completion repro (advance: "all") where
ask, route and onRejected all fired correctly and the items were empty
afterwards — and it would strand identically under the worker; the sync
path was just the only one an e2e drove through a router.
FlowTokenRouter::resolveOutputTags() now rewrites exit-id tags to the
exit's place at all three firing sites, before the exit choice and the
per-place delivery. takenExits() additionally honours the routing tag
among UNCONDITIONED exits: the tagged exit beats declaration order, so the
token follows the node's own routing decision instead of landing on the
first declared exit for the wrong reason. Conditioned exits (Switch) are
unchanged and still win.
Regression: FlowLogicTest::testAnItemTaggedWithAnExitIdLandsOnTheExitsPlace
and ::testTheTaggedExitBeatsDeclarationOrder (both red before), plus
FlowRunServiceAdvanceStreamTest::testTheSyncAdvanceCarriesTheStoredItemsInAndPersistsTheWalkItemsOut
pinning the sync persistence seam; tests/e2e/api-direct/flow-user-task.spec.ts:429
is the end-to-end regression on the next rig reset.
* fix(task): the inbox reads the subject's real serialised shape, so rows carry their case context
ObjectEntity::jsonSerialize() puts the object's own data at top level and
its identity under @self (uuid keyed id, mirrored as a top-level id); there
is no flat uuid/register/schema/name key. TaskInboxService::subjectContexts()
read exactly those flat keys, so the uuid check failed for every real object
and every inbox and portal row shipped subject: null — flow-portal-task's
seam contract says the row carries its case context, and
flow-portal-task.spec.ts:383 pinned the gap deterministically. The unit
test's hand-rolled stub returned the flat shape the entity never emits, so
it agreed with the exact bug it should have caught.
The context now reads @self first (uuid via @self.id / top-level id), with
the flat keys kept as fallbacks for stores that serialise flat. The stub is
replaced with a real ObjectEntity, and a portal-level test builds the inbox
WITH an object store — the portal unit tests used to omit it entirely,
which made subject structurally null in every one of them.
Regression: TaskInboxServiceTest::testSubjectContextIsBatchedForThePage
(now over a real ObjectEntity; red before) and
PortalTaskServiceTest::testAPortalRowCarriesItsCaseContext; the e2e at
flow-portal-task.spec.ts:383 goes green on the next rig reset.
* fix(flow): one unpublished flow no longer silences every flow on its event
Two defects with one blast radius. FlowTriggerService::fire() held its
try/catch OUTSIDE the per-flow loop, so the queue path's refusal of one
enabled-but-unpublished flow ('This flow has no published version') aborted
queuing for every healthy flow wired to the same event — the poisoned case
got no run from the flow that would have served it. The loop now isolates
per flow: a queue refusal logs against THAT flow and the rest queue; a sync
flow's inline-execution failure gets its own catch too, because by then the
run exists and the worker drains it (the count now says so — reporting zero
for a queued run was the instrument lying, and one execution-mode test pin
is updated accordingly).
And the delivery mechanism: FlowLocator's column fallback treated 'has no
index rows' as 'unconverted, columns decide', which is indistinguishable
from 'unpublished, subscribed to nothing'. flow-definition-versioning is
explicit — only a flow's published version contributes trigger records, a
draft's trigger nodes match nothing, and enabled is orthogonal to lifecycle
(so enabled:true pre-publish is neither indexed nor refused; the create and
enable paths already both index nothing pre-publish, which
FlowTriggerIndexPublishedTest pins). The fallback now refuses, out loud, a
flow with no published version; legacy flows all hold a backfilled
published v1, so the only thing filtered is a flow that could never have
run. Unreadable version rows fail open — the queue path still decides.
Regression: FlowTriggerServiceTest::testOneUnpublishedFlowDoesNotAbortTheFanOutForItsSiblings
and FlowLocatorTriggerCutoverTest::testAnUnpublishedFlowIsFilteredFromTheColumnFallback
(both red before this change).
* refactor(flow): keep the analyzers' thresholds after the trigger and routing fixes
The fixes pushed four methods and one class exactly onto their PHPMD
thresholds. Extract rather than suppress: the column-fallback loop moves to
FlowLocator::columnFallbackMatches() with the two-rule docblock, the
per-object context read moves to TaskInboxService::contextRow(), the
router's tag set and tag match get named helpers, and one over-long log
line wraps. Behaviour is pinned unchanged by the existing suites.
* test(flow): list FlowVersion in the uses rosters the locator seam now exercises
* fix(quality): wrap the second over-length log line and cover the new seams' branches
CI's phpcs names line 137 of FlowTriggerService (151 chars): the inline
failure message gained one indent when it got its own catch, after the
local phpcs pass. Wrapped like its sibling.
The pgsql cell's coverage guard measures the code this change keeps or
adds, and the fixes added statements whose failure branches no test
walked. Covered now, each in the suite that owns the seam: the
subject-projection refresh's identity fallbacks and no-ops
(FlowItemsRefreshTest), the per-place buffer rewrite on resume
(FlowRunServiceTest), the locator's fail-open on an unreadable version
table (FlowLocatorTriggerCutoverTest), the router's tag resolution
across multi-place, unroutable and already-resolved tags plus the
tag-over-declaration-order choice (FlowTokenRouterEndpointsTest), and
the inbox's flat-serialisation fallback (TaskInboxServiceTest).
* test(flow): count the trigger and locator branch coverage the strict cell was discarding
The coverage guard still read the fan-out isolation and fallback-filter
branches as uncovered: their pins live in files with docblock rosters, and
beStrictAboutCoverageMetadata marks a roster'd test risky the moment it
executes an unlisted class, discarding that test's WHOLE coverage. So the
cutover suite's refusal branch and the execution-mode suite's inline paths
counted for nothing (CI's coverage run shows Risky: 314).
Two-sided fix. The rosters are completed where they were short (Db\Flow on
the cutover suite; Db\FlowRun and FlowItems on the execution-mode suite),
and the branches are additionally exercised from roster-free suites, which
the cell always counts: the locator's unpublished-flow refusal and version-
table fail-open in FlowLocatorTest, the locator-failure and inline-failure
catches plus the subjectless payload seed and unresolvable-subject paths of
runInline in FlowTriggerServiceTest, the router's malformed/unwired exit
skips, and the getObject() subject shape in FlowItemsRefreshTest.1 parent 82ed322 commit 14bd30f
20 files changed
Lines changed: 1540 additions & 49 deletions
File tree
- lib/Service
- Flow
- Task
- tests/Unit/Service
- Flow
- Portal
- Task
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
490 | 490 | | |
491 | 491 | | |
492 | 492 | | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
493 | 496 | | |
494 | 497 | | |
495 | 498 | | |
| |||
657 | 660 | | |
658 | 661 | | |
659 | 662 | | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
660 | 669 | | |
661 | 670 | | |
662 | 671 | | |
| |||
1075 | 1084 | | |
1076 | 1085 | | |
1077 | 1086 | | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
1078 | 1091 | | |
1079 | 1092 | | |
1080 | 1093 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
209 | 209 | | |
210 | 210 | | |
211 | 211 | | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
212 | 299 | | |
213 | 300 | | |
214 | 301 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
| |||
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
| 70 | + | |
69 | 71 | | |
70 | 72 | | |
71 | 73 | | |
72 | 74 | | |
73 | 75 | | |
74 | 76 | | |
| 77 | + | |
75 | 78 | | |
76 | 79 | | |
77 | 80 | | |
| |||
221 | 224 | | |
222 | 225 | | |
223 | 226 | | |
224 | | - | |
225 | | - | |
226 | | - | |
227 | | - | |
228 | | - | |
229 | | - | |
230 | | - | |
231 | | - | |
232 | | - | |
233 | | - | |
234 | | - | |
235 | | - | |
236 | | - | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
237 | 231 | | |
238 | 232 | | |
239 | 233 | | |
| |||
259 | 253 | | |
260 | 254 | | |
261 | 255 | | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
262 | 335 | | |
263 | 336 | | |
264 | 337 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
269 | 269 | | |
270 | 270 | | |
271 | 271 | | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
272 | 277 | | |
273 | 278 | | |
274 | 279 | | |
| |||
823 | 828 | | |
824 | 829 | | |
825 | 830 | | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
826 | 837 | | |
827 | 838 | | |
828 | 839 | | |
| |||
895 | 906 | | |
896 | 907 | | |
897 | 908 | | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
898 | 963 | | |
899 | 964 | | |
900 | 965 | | |
| |||
0 commit comments