Repository navigation
Commit 21e5979
fix(trigger-schedule): an invalid timeRelative descriptor throws from start so the automation status reads it unbound (#27556)
Fixes #27415
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:时间相对触发器(time_relative)在 `timeRelative` 描述符不合法时,`start` 由「记日志后
return」改为「记同一句日志后抛错」,并先撤掉此前已挂的扫描任务。
- 为什么改:原来 return 被引擎当成绑定成功,`GET /automation/_status` 对一个根本没挂上的流报 `bound:
true`、没有 `unboundCause`,与服务器自己的 "not bound"
日志相矛盾;抛错是引擎既有的「绑定失败」通道,`schedule` 触发器的组织/部署策略拒绝早已这样做。
- 风险与回滚:只改一个文件的一个分支;合法描述符行为不变。直接调用 `TimeRelativeTrigger.start`
的宿主在非法描述符上会收到异常(引擎自身已 catch)。回滚即还原本 PR 一个 commit。
-
先看:`packages/triggers/trigger-schedule/src/time-relative-trigger.ts`(`start`
里 `!parsed.success` 分支,风险在 throw 前的
`this.stop`)、`src/time-relative-bind-status-e2e.test.ts`(真引擎状态行的 pin)。
- 两分钟验证:`pnpm --filter @objectstack/trigger-schedule exec vitest run
src/time-relative-bind-status-e2e.test.ts`,预期 1 passed:非法描述符的流读 `bound:
false` + `unboundCause: binding_failure`,合法的流读 `bound: true`。
- 需要你做的:无,席位落地。
## What changes
`TimeRelativeTrigger.start` refused an invalid `config.timeRelative`
descriptor (both `withinDays` and `offsetDays`, an unknown key,
`schedule` nested inside the descriptor) by logging `[time-relative]
flow 'NAME' has no valid timeRelative descriptor — not bound` and
returning. `FlowTrigger.start` returns `void`, so the engine's
`activateFlowTrigger` read that return as a successful arm and recorded
the flow in `boundFlowTriggers`. The status row
(`getFlowRuntimeStates()`, served by `GET /api/v1/automation/_status`)
then read `bound: true` with no `unboundCause`.
The refusal now throws the same sentence it logs. This is the path the
trigger's own organization and deployment-policy refusals already take
(`refuseMissingOrganization` / `refuseScheduledWorkDisabled` in
`schedule-trigger.ts`; the "Why it throws, and does not merely log and
return" docblock there describes the mechanism). The engine's existing
catch never marks the flow bound, so the row reads `bound: false` with
`unboundCause: 'binding_failure'`, and the boot binding audit lists it.
Before throwing, the branch calls `this.stop(flowName)`, as the
organization refusal does, so a re-bind with a broken descriptor cannot
leave the sweep from an earlier valid bind armed. The `warn` line text
is byte-identical to before, and the level stays `warn`: a trigger that
is not armed is a functional degradation, and the status row now reports
it.
No engine change and no `FlowTrigger` interface change.
`packages/services/service-automation/**` is untouched.
One `patch` changeset for `@objectstack/trigger-schedule`.
## Tests
- `src/time-relative-trigger.test.ts`: the existing "invalid descriptor"
case now asserts the throw. A new block covers three invalid shapes
(both modes, an unknown key, `schedule` nested in the descriptor). For
each: `start` throws a message naming the flow, arms no job, and logs
the refusal once. Also new: a re-bind with a broken descriptor drops the
previously armed sweep, and a control where a valid descriptor does not
throw and arms its sweep.
- `src/time-relative-bind-status-e2e.test.ts` (new): the real
`TimeRelativeTrigger` registered on the real `AutomationEngine`. An
invalid-descriptor flow reads `bound: false`, `triggerType:
'time_relative'`, `unboundCause: 'binding_failure'`, with no sweep
armed. The valid control flow reads `bound: true`, has no `unboundCause`
key, and its sweep is armed. The engine-only twin of this pin
(`engine.test.ts`, "ON: a genuine bind failure reads as one on the
status row too") lives in `service-automation`, which is outside this
PR's surface. This package already devDepends on that engine, so the pin
lives here.
<details><summary>Package test + typecheck at da85763</summary>
```
pnpm --filter @objectstack/trigger-schedule test && pnpm --filter @objectstack/trigger-schedule typecheck
Test Files 11 passed (11)
Tests 196 passed (196)
> tsc --noEmit
os-verify-lock: VERDICT command-exit 0
```
`tsc --listFiles` counts 11 of the package's `*.test.ts` files in the
program, including the new e2e file.
</details>
<details><summary>Ablation (one-shot): the throw replaced by the old
return</summary>
`node scripts/ablation-replace.mjs --file
packages/triggers/trigger-schedule/src/time-relative-trigger.ts --anchor
'throw new Error(sentence);' --replacement 'return;'` landed (anchor 1
to 0, blob 520bd25 to cf719226888f). Both suites then went red:
```
Test Files 2 failed (2)
Tests 6 failed | 60 passed (66)
x does not bind when the timeRelative descriptor is invalid
x both windowing modes / an unknown key / schedule nested: start throws ...
x a re-bind with a broken descriptor drops the sweep the previous bind armed
x an invalid descriptor reads bound:false with the binding-failure cause; a valid one reads bound:true
```
Restore leg: blob after restore 520bd25 equals the HEAD blob, and
`git diff HEAD` is empty. The trigger is imported by relative path, so
the mutation reached the run without a rebuild. The engine resolves
through `dist/`, but its source is unchanged in this diff.
</details>
<details><summary>Local gates (derived by dispatch-gates
--commands)</summary>
57 derived families were run locally, and `--ran` reconciled 57 of 57.
54 exited 0. `check:nul-bytes`, `check:cross-package-test-inputs`,
`check:test-source-alias` and `check:engine-double-contract` were re-run
at da85763 and all exited 0. NOT MEASURED locally, left to CI:
- `check:dual-build-cjs-loads`: exit 3, PREREQUISITE NOT MET. It needs
every package's `dist/`.
- `check:query-options-erasure`: hit the 300 s local timeout. It is a
repo-wide ESLint scan.
- `check:type-check-debt`: hit the 300 s local timeout during the
whole-repo tsc walk.
The derivation warned that its tree was 7 commits behind origin/main.
The 2 stale files are `scripts/gen-sdui-reference.mjs` and
`scripts/pm/dispatch-gates.self-test.mjs`. Neither reads this diff's
paths.
</details>
## Acceptance notes
- The `schedule` trigger's own descriptor refusal has the same
log-and-return shape: `[schedule] flow 'NAME' has no recognizable
schedule descriptor — not bound` in `ScheduleTrigger.start`
(`schedule-trigger.ts`). Under this PR's reasoning that flow also reads
`bound: true`. That file is outside this PR's surface, so the seat has
the finding to route.
- Both triggers' "job service unavailable — not scheduled" branches also
return rather than throw. That is a composition gap, a different remedy
from a bad descriptor. This is noted only: it was inferred from reading
the code and not reproduced.
## Verification
Run the two-minute check above. Or, as on the card: boot the showcase
with `OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1` and
`OS_ALLOW_UNLINTED_METADATA_WRITES=1`, `PUT` a time_relative flow whose
descriptor carries both `withinDays` and `offsetDays`, then read `GET
/api/v1/automation/_status`. Expect `bound: false` with `unboundCause:
"binding_failure"`. This live-boot path was not run in this PR, and the
e2e pin above covers the same engine row.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 475d428 commit 21e5979
4 files changed
Lines changed: 199 additions & 17 deletions
File tree
- .changeset
- packages/triggers/trigger-schedule/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
403 | 403 | | |
404 | 404 | | |
405 | 405 | | |
406 | | - | |
| 406 | + | |
| 407 | + | |
407 | 408 | | |
408 | 409 | | |
409 | 410 | | |
410 | 411 | | |
411 | 412 | | |
412 | 413 | | |
413 | | - | |
414 | | - | |
415 | | - | |
416 | | - | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
417 | 425 | | |
418 | | - | |
419 | | - | |
420 | | - | |
421 | | - | |
422 | | - | |
423 | | - | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
424 | 476 | | |
425 | 477 | | |
426 | 478 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
334 | 334 | | |
335 | 335 | | |
336 | 336 | | |
337 | | - | |
338 | | - | |
339 | | - | |
340 | | - | |
341 | | - | |
342 | | - | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
343 | 359 | | |
344 | 360 | | |
345 | 361 | | |
| |||
0 commit comments