Uh oh!
There was an error while loading. Please reload this page.
fix(runtime): return the declarative job handler's JobRunOutcome so a degraded run is recorded - #14711
Merged
Merged
Conversation
Red-first: this regression test fails against the discarding `await` in
AppPlugin's declarative-job wrapper. A declarative job resolving
`{ outcome: 'degraded', reason }` must land a `sys_job_run.status` distinct
from `success`, driven through the real `DbJobAdapter` over a real ObjectQL
engine carrying the real `sys_job` / `sys_job_run` declarations.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza) AppPlugin's declarative-job wrapper awaited the bundle handler inside a block-bodied arrow and returned nothing, so it was a `Promise<void>` whatever the handler resolved. #6617's third outcome was therefore unreachable from `defineJob`: a job that ran to completion while its work did not happen was recorded as `success` with `reason` dropped, while all three shipped adapters map a resolved `{ outcome: 'degraded' }` onto a run status distinct from it. Also drops the read-refusal capture from the new pin's rig: measured on the red run, no path in that file provokes the tenancy probe, and a capture nothing provokes asserts a mute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…clarative-job-outcome
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-trump
marked this pull request as ready for review
September 2, 2026 20:15
os-trump
enabled auto-merge
September 2, 2026 20:15
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#14256
AppPlugin's declarative-job registration block handedIJobService.scheduleablock-bodied arrow that awaited the bundle handler and returned nothing, so the wrapper
resolved
undefinedwhatever the handler resolved.JobHandlerhas been declared asresolving either
voidor aJobRunOutcomesince #6617, and all three shipped adapters(
cron-job-adapter,interval-job-adapter,db-job-adapter) map a resolved{ outcome: 'degraded', reason }onto a run status distinct fromsuccess— they simplynever received the value on this path. The reporter's probe, driven through an
IJobServicetyped only at the contract:So a declarative job that ran to completion while its work did not happen (store
unavailable, zero rows matched) was recorded as
successwithreasondropped, and thethree-outcome table in
content/docs/automation/jobs.mdx— the page whose whole subjectis the declarative door — was false on exactly that door. The imperative route (a handler
registered straight on
IJobService.schedule) was unaffected throughout; the wrapper isthe whole defect. Per the card, the page is deliberately left alone: the code is the
honest repair.
The change
packages/runtime/src/app-plugin.ts—return await handler(jobContext);in place ofthe discarding
await, with a comment naming why the value is load-bearing. That is thewhole production diff.
The deliverable: the regression test
packages/runtime/src/app-plugin.job-degraded-outcome.test.ts(6 cases) pins theconsequence, not the repair. It boots a real
ObjectQLengine over the migratedsqlite
:memory:backend carrying the realsys_job/sys_job_rundeclarations,registers the job through
AppPluginfrom adefineStack-shaped bundle, and runs it theway the scheduler does —
DbJobAdapter.trigger, never a direct handler call. Everyprimary assertion reads the persisted cell:
sys_job_run.statusasserted not to besuccessfirst (the card's own wording, andthe contract's), then to be
degraded, with the reason inerror;sys_job.last_status/last_error, withfailure_countflat —degraded is not a failure and never retries;
{ handler, effect }function form.Kept in
packages/runtimerather thanpackages/services/service-job: the defect is inAppPlugin, and@objectstack/service-jobis already a devDependency of@objectstack/runtimeand is aliased to source by that package'svitest.config.ts, sothe recording adapter is reachable from the package that owns the bug. Driving the real
engine (rather than a recording double) also proves the status the adapter writes is a
value the enforced
sys_job_run.statusField.selectvocabulary accepts, and it adds noengine double to the
check:engine-double-contractledger.Two in-suite controls keep the assertion honest, plus one corroborating middle term:
IJobService.schedulerecordsdegradedwith noAppPluginin the path. Green beforethis change and green after, so a red on the declarative cases localises to the wrapper
rather than to
DbJobAdapteror thesys_job_runwrite;success, which is also whatrules out the rig writing
degradedunconditionally;IJobServiceresolves the handler's outcome. Corroboration only: on its own it would re-state the
repair rather than pin its consequence.
The rig deliberately installs no expected-read-refusal capture: measured on the red
run, every read in the file carries
isSystem, the tenancy probe never fires, and neitherrefusal channel emits a frame — a capture nothing provokes would assert a mute.
Red-first, with the mutation and the restore both proven
Ablation run from the committed state, as one script holding the shared verify lock, with
a
trap … EXIT INT TERMon an absolute path:The mutation is proven by content — the removed text counted to zero and the injected
text counted to one, separately — never by an editor's exit code; the restore is proven by
blob identity against the
HEADblob plus an emptygit diff HEADand a cleangit status --porcelain. The split is the part that matters: the four cases that go redare the declarative ones, while both controls stay green under the same mutation. No
rebuild leg applies — the mutated file is imported relatively by the pin, so vitest reads
source, and
packages/runtime/distwas absent at the time (asserted in the run above).Green after the fix:
Test Files 1 passed (1) · Tests 6 passed (6).Verification
Everything below ran on the final tree,
git rev-parse --short HEAD=0f4a80ee47, aftermerging
origin/mainand re-deriving.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands. Thefirst derivation reported a stale tree, so
origin/mainwas merged, the install and thedependency closure refreshed, and it was re-derived on the merged tree — 36 commands,
the same list. Each ran with its exit captured before any pipe
(
cmd > log 2>&1; EXIT=$?): 32 exit 0.red:
check-test-completeness— "There is no local log to hand it, so the local readingfor this gate is NOT MEASURED";
check-half-states— "Nothing was swept … it is noreading at all" (this session's repo-scoped REST reads answer 403);
check:dual-build-cjs-loads— "PREREQUISITE NOT MET — this gate reads built output, andsome package has no dist/";
check:type-check-debt— "PREREQUISITE NOT MET … 26workspace dependenc(ies) of the ledgered packages have no built type entry point on
disk". All four want a full-workspace build, which CI performs.
pnpm --filter @objectstack/runtime test—Test Files 210 passed (210) · Tests 3085 passed (3085), VERDICTcommand-exit 0fromthe shared verify lock.
pnpm --filter @objectstack/runtime typecheck— exit 0.narrowly: that script is
tsc --noEmitover atsconfig.jsonthat excludes**/*.test.ts, so it coversapp-plugin.tsand says nothing about the new testfile. Confirmed with
--listFiles:app-plugin.tspresent, the test file absent.Compiled separately under the package's own options with tests included, the new file
reports zero errors (the package's pre-existing test-layer errors are the subject of
the standing finding [finding] packages/runtime type-checks none of its 208 test files — tsconfig excludes **/*.test.ts and no test-typecheck wiring exists #14504 and are untouched here).
pnpm lintwas attempted through the sharedverify lock three times and returned
exit 99(queue-timeout, "never acquired") everytime, holder
pid 2524from another dev's run, held 755s at the last attempt — so it isNOT MEASURED locally, and CI's
Lint & Repo Gatesruns the farm regardless. What wasmeasured instead, with the three things a narrowing owes:
calculateConfigForFileplus
isPathIgnoredover every tracked file, no rule executed): 5,749 files, andboth changed TS files are members;
--format json:eslint --no-inline-config --format jsonon the two changed TS files reports2 files, 0 errors, 0 warnings, exit 0;
eslint.config.mjsstates it in its own words,"no
parserOptions.project, no typed@typescript-eslintrules" — so this diffcannot move the verdict on any file it does not contain. The changeset
.mdisoutside the population by ESLint's own config, not by assumption.
Scope
packages/runtime/src/app-plugin.tsplus its new pin and the changeset — three files,nothing else.
content/docs/automation/jobs.mdx,content/docs/releases/**,docs/adr/**and the other governed surfaces are untouched, and none of the
packages/runtimefilesfenced as held by other in-flight branches was edited.
⛔ Draft on purpose: the PM seat lands it. Not flipped ready, no auto-merge.
🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code
Generated by Claude Code