Fix discarding identification and audit - #6841
Conversation
…ification Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
openBuilder() returns as soon as navigation does, and the shell's scripts are deferred, so the probe for Alpine and the Builder's stores ran against a page that had not finished booting. On a slow runner that reports a healthy shell as broken - which is what it did on this branch's smoke run, in a test that has nothing to do with the change under review. Poll the same condition to a 30s deadline. A mis-ordered or missing script never registers the stores, so a real breakage still fails; it just fails at the timeout rather than instantly. Polling happens in the calling thread because Selenide binds its WebDriver per-thread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tches
Three PRs in a row failed their smoke run on assertions that were right about the
generator and wrong about how they looked at it. All three were the same two
hazards, so fix the hazards rather than the sites.
Comments. The events templates carry long explanations that necessarily name the
very calls and topics being asserted on, so a contains() could be satisfied by
prose the code never contained - and an assertFalse() tripped by prose the code
never earned. Read generated Java through codeOf(), which strips comments with a
scanner rather than a regex, because a "//" inside a literal (an endpoint URL, a
JSON template) would otherwise cut real code away. Three assertions turned out to
be about prose on purpose - a scaffold naming its field in the javadoc, a
developer's preserved marker, a guard matched by its trailing comment. The first
two stay on contentOf() and say why; the third now matches the guard itself.
First matches. indexOf() answers with the first hit and says nothing about a
second, so an anchor that becomes ambiguous silently relocates the assertion. That
is how the number stamp's reload assertion came to be about its guard read
instead. Ordering assertions now go through onlyIndexOf(), which fails at the
anchor when it is not unique.
Coverage. ModelGenerationIT exists to catch unresolved ${...} in rendered output,
but its fixture declared no resolves, writers or numbering - three of the four
templates these PRs touched were never rendered by it. Adding them is fixture-only
and verified red-first: dropping setProperty, keyAccessor and series each surfaces
as an unresolved reference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous push updated this PR's head but GitHub created no workflow run for it, leaving the PR displaying the previous head's conclusion. Empty commit to fire pull_request.synchronize so the checks run against the current code.
…arding # Conflicts: # components/engine/engine-intent/CLAUDE.md
One file conflicted, Resolve.java.template, three times - and the two changes are complementary rather than competing: - this branch splits stamp() into two targeted writes so a rejected status move can no longer discard the resolved relation and the audit trace; - master's eclipse-dirigible#6836 publishes "-transitioned" after a routing so an AUTOMATIC resolution reaches the constructs a manual transition does. Both kept. The publish is placed INSIDE the try, immediately after the successful updateProperty: a status the record could not take is not a transition, so a rejected move must announce nothing - master's version sat after a single combined write, where that distinction did not yet exist. The two javadoc blocks are merged the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Merged current One file conflicted —
The one judgement call: #6836's publish sat after a single combined write, where "the status was written" and "the method succeeded" were the same thing. With the write split they are not, so the publish now sits inside the That placement also keeps this branch's own emission assertions valid: Verified on the merged tree before pushing: |
Two separate races against one cold page load, both of which report a healthy Builder as broken. openBuilder() returns as soon as navigation does and the shell's scripts are deferred, so the probe for Alpine and the Builder's stores sampled a page that had not finished booting. That is the failure seen on this PR's smoke run and on eclipse-dirigible#6841 - a test with nothing to do with either change. It now polls the same condition instead; a mis-ordered or missing script still never registers the stores, so a real breakage still fails, just at the deadline rather than instantly. The second race had not been drawn yet. This class configures no Selenide timeout, so every shouldBe without an explicit Duration gets the library default of four seconds - and three of them wait on elements Alpine renders after those stores register, one straight after a page load. Four seconds there is a race against the boot, not a check on it. They get explicit deadlines, in line with every other wait in the class. Left alone: the waits that follow an already-satisfied wait on the same rendered region. The DOM is present by then, and inflating every timeout only makes a genuine breakage take minutes to surface instead of seconds.
Two separate races against one cold page load, both of which report a healthy Builder as broken. openBuilder() returns as soon as navigation does and the shell's scripts are deferred, so the probe for Alpine and the Builder's stores sampled a page that had not finished booting. That is the failure seen on this PR's smoke run and on eclipse-dirigible#6841 - a test with nothing to do with either change. It now polls the same condition instead; a mis-ordered or missing script still never registers the stores, so a real breakage still fails, just at the deadline rather than instantly. The second race had not been drawn yet. This class configures no Selenide timeout, so every shouldBe without an explicit Duration gets the library default of four seconds - and three of them wait on elements Alpine renders after those stores register, one straight after a page load. Four seconds there is a race against the boot, not a check on it. They get explicit deadlines, in line with every other wait in the class. Left alone: the waits that follow an already-satisfied wait on the same rendered region. The DOM is present by then, and inflating every timeout only makes a genuine breakage take minutes to surface instead of seconds.
# Conflicts: # tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java
…6867) With #6841 merged, the lookup's routing status was a targeted updateProperty followed by a post-commit re-read and a bare publish of "-transitioned" - the same write-beside-publish pair the rest of this branch removes. The topic now rides the routing write into the outbox: the flip and its announcement commit together, the payload is the row exactly as the statement left it, and a rejected move (the ValidationException the routing write catches) records no event at all - a status the record could not take is not a transition. The emission oracles pin the new contract: IntentEngineIT's two-writes ordering assertion anchors on the topic-carrying updateProperties, and IntentEmissionCoverageIT's resolve block asserts the topic on the write and refuses any bare Producer.sendToTopic in the generated lookup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nce guard (#6859) * feat: Implementing appUrl inside the notify subject/body interpolation * feat: Implementing tenant config overrides into client-Java listener dispatch * Refuse two event-driven rules that share one at-most-once guard The guard the templates emit is findAll(eq(<backReference>, sourceId)): it asks whether the source already has a row through that relation, and cannot tell which rule wrote it. So two event-driven `generates:` / `posts:` rules sharing a target AND a back-reference divided silently into a winner and a loser - whichever fired first claimed the source forever, and the other handed back that row instead of writing, for that source and every future one. Nothing surfaced it. It parsed, generated and compiled, and the loser read as a rule whose condition never matched. Disjoint `when:` guards did not save it either, which is the trap: the author writes two mutually exclusive conditions and gets two independent rules, but the collision is decided by the target's EXISTENCE, so the second no-ops on a source whose condition it matched perfectly. Both halves of the key are static in the model, so refuse it at parse time and name the ways out - separate back-references, separate targets, or `mode: append` on every one of them. Two decisions worth recording. The check spans both constructs rather than being one per collection, because a `posts:` row satisfies a `generates:` guard just as well. And `mode: append` is not simply exempt: two appending rules genuinely cannot collide, since neither reads the other's rows, but append PLUS guarded does - the appended rows carry the back-reference, and that is all the guarded rule's lookup needs to be satisfied forever. That pairing is the case #6800 leaves open, and it has its own test. * Stop IntentBuilderShellIT racing the shell it is testing Two separate races against one cold page load, both of which report a healthy Builder as broken. openBuilder() returns as soon as navigation does and the shell's scripts are deferred, so the probe for Alpine and the Builder's stores sampled a page that had not finished booting. That is the failure seen on this PR's smoke run and on #6841 - a test with nothing to do with either change. It now polls the same condition instead; a mis-ordered or missing script still never registers the stores, so a real breakage still fails, just at the deadline rather than instantly. The second race had not been drawn yet. This class configures no Selenide timeout, so every shouldBe without an explicit Duration gets the library default of four seconds - and three of them wait on elements Alpine renders after those stores register, one straight after a page load. Four seconds there is a race against the boot, not a check on it. They get explicit deadlines, in line with every other wait in the class. Left alone: the waits that follow an already-satisfied wait on the same rendered region. The DOM is present by then, and inflating every timeout only makes a genuine breakage take minutes to surface instead of seconds. --------- Co-authored-by: Nedelcho Delchev <delchevn@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…h their writes (#6869) * fix(templates): the rekey pair and the transition announce commit with their writes (#6867) #6854 established the contract - announce an entity change by handing the topic to the WRITE, never by publishing beside it - and converted the generated repository's main paths. Two write+publish pairs survived it, each a live instance of #6816's failure class (row committed, event lost for good when the broker is briefly down, downstream reactions silently never run): - The DAO's targeted "-rekeyed" pair (#6845): one mutation had to record TWO bodies - the row as it stood, naming the group it left, and the row as written, naming the one it joined - and the targeted write could carry only one topic. The store and JavaRepository gain the event-carrying overloads (save and updateProperties with additionalEvents, mirroring update's trio), recorded only when the row actually existed to be written; the template hands both notices to the base write. A model without grouping keys regenerates byte-identically. - The transition controller (Transition.java.template): updateProperty + post-commit re-read + bare publish becomes one targeted write carrying the "-transitioned" topic, so the flip and its announcement commit together and the event payload is the row exactly as the statement left it - never a re-read a concurrent write could have moved on. The reload stays only for the HTTP response and the notify block. Deliberately NOT converted here, with the reasoning on #6867: the five deferred/ordered announces (SetField, Writer, Numbering, StepEvent via Process.executeAfterCommit - deferred so consumers observe the whole synchronous Flowable chain's writes, which write-attachment would regress - and Generate's completion announce, ordered after the target and its items across several transactions). Those need a durable-enqueue primitive, a follow-up of its own. Resolve.java.template follows once #6841, which rewrites that exact region, lands. JavaEventOutboxIT gains the retarget phase - one targeted mutation, two notices, both delivered and both cleared - and IntentEmissionCoverageIT pins the new contract, including that no bare Producer.sendToTopic remains in a generated repository or transition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(templates): the resolve routing announce commits with its write (#6867) With #6841 merged, the lookup's routing status was a targeted updateProperty followed by a post-commit re-read and a bare publish of "-transitioned" - the same write-beside-publish pair the rest of this branch removes. The topic now rides the routing write into the outbox: the flip and its announcement commit together, the payload is the row exactly as the statement left it, and a rejected move (the ValidationException the routing write catches) records no event at all - a status the record could not take is not a transition. The emission oracles pin the new contract: IntentEngineIT's two-writes ordering assertion anchors on the topic-carrying updateProperties, and IntentEmissionCoverageIT's resolve block asserts the topic on the write and refuses any bare Producer.sendToTopic in the generated lookup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…nt predicate (#6858) * feat: Implementing appUrl inside the notify subject/body interpolation * feat: Implementing tenant config overrides into client-Java listener dispatch * Let a register lookup filter the register with a constant predicate A `resolves:` lookup could not narrow its register by a literal. Every `match:` pair binds a register column to a column of the RECORD, so "and only the rows that are still valid" had no form at all - and a register is exactly the kind of table that keeps its corrections. The cancelled row stays beside the active one over the same dates, so both cover the violation date, and a lookup with one right answer reports `ambiguous`, routes to manual handling and logs "multiple matches". Every correction ever made earns that, quietly, and it gets worse the longer the register lives. Add an optional `where: { <register property>: <literal> }`, ANDed onto the same Criteria as the match keys so it narrows the query rather than being applied after the period comparison. Three things are deliberately not a mirror of the relation-level `where:`. Multiple pairs are allowed: that one is capped at a single pair because it lands in two EDM attributes, whereas these are chained `eq` calls where a second condition costs nothing. A pair naming the register's `function: EntityStatus` relation may use the seeded NAME, resolved on the REGISTER's own nomenclature - the record's would hand back a plausible id from the wrong lifecycle - and only that pair is offered to the resolver, so an ordinary `kind: PRIMARY` is not reported as an unknown status. And a pair repeating a `match` key is refused rather than ANDed: on a column already bound to the record a literal either repeats the match or contradicts it into matching nothing, and which one depends on data the parser cannot see. The generated handler names the filter in its javadoc and in the notFound log, so a filter that is too narrow reads as a filter rather than as missing data. Also two defects in the javaLiteral helper this reuses: a Boolean was quoted into the string "true", which matches no boolean column, and a backslash was not escaped. * Stop IntentBuilderShellIT racing the shell it is testing Two separate races against one cold page load, both of which report a healthy Builder as broken. openBuilder() returns as soon as navigation does and the shell's scripts are deferred, so the probe for Alpine and the Builder's stores sampled a page that had not finished booting. That is the failure seen on this PR's smoke run and on #6841 - a test with nothing to do with either change. It now polls the same condition instead; a mis-ordered or missing script still never registers the stores, so a real breakage still fails, just at the deadline rather than instantly. The second race had not been drawn yet. This class configures no Selenide timeout, so every shouldBe without an explicit Duration gets the library default of four seconds - and three of them wait on elements Alpine renders after those stores register, one straight after a page load. Four seconds there is a race against the boot, not a check on it. They get explicit deadlines, in line with every other wait in the class. Left alone: the waits that follow an already-satisfied wait on the same rendered region. The DOM is present by then, and inflating every timeout only makes a genuine breakage take minutes to surface instead of seconds. * fix(ide-template): a .glue written before the where: filter keeps rendering (#6813) GlueGenerator.copy REMOVES an absent key, and Velocity renders the removed reference as its own literal - so a resolves entry written before the where: keys existed emitted ${filterSummary} into the generated javadoc and read as filtered while filtering nothing. The binder now defaults filters to the empty list and filterSummary to "" - the no-filter shape, the same migration pattern the create-from's topicSuffix uses. Caught by ModelGenerationIT's unresolved-reference guard on the pre-existing fixture entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Nedelcho Delchev <delchevn@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This PR:
resolves:throws away the identification when the status move is rejectedresolves:batched three semantically independent values — the resolved relation, theoutcome:trace, and the routing status — into ONE
updatePropertiescall. The generated DAO runs thelifecycle:andchecks:gates against the post-write row before persisting anything(
Repository.java.template: values applied in memory →enforceLifecycleMove→enforceChecks→super.updateProperties), so a rejected status move discarded the relation and the trace with it.The system did the work, got the right answer, and threw all of it away — including the audit field
whose entire purpose is recording what happened. And because the listener dispatcher gave no
explanation, a reviewer could not tell this apart from "the lookup never ran".
The fix
stamp()now writes twice, in this order:read, exactly one row covered the date, this is the answer.
A rejected transition can no longer destroy the identification, and the audit trace is the last
thing a failure can take.
The routing write catches
ValidationExceptionspecifically. Retrying cannot help — nothing aboutthe record changes by re-reading the register — so rethrowing would only burn the redelivery budget
(now real, since #6830) and dead-letter. Instead it logs and amends the trace to
<outcome>-notRouted, so a routed-but-rejected record stays distinguishable from a fully processedone. A genuine DB failure is not caught and still propagates and retries.
Both writes stay targeted, so no
-updatedre-fires and no concurrent write to another column isreverted.
Also here
A parser guard that the
outcome:field is long enough for the values written — 9 normally, 19 onceany outcome routes by
setStatus. Truncation happens at the DB where nothing reports it, whichwould have reintroduced the same silent loss through the back door. Only bites an author who
explicitly narrows the field (the default VARCHAR length is 100).
Verification
IntentEngineIT.resolve_writes_the_result_before_the_routing_status— asserts the write order, thatthe status is not in the batched map, the
ValidationExceptioncatch, the amended trace, andthe log line.
ResolveIntentTest.rejectsAnOutcomeFieldTooShortForTheValuesWritten— red-first verified; alsoasserts the non-routing case still accepts a shorter field.
formatter:validateclean.#ifshapes (writesStatus×outcomeProperty).Not run: the new IT.
engine-camel's Maven plugin is unresolvable in my offline environment,which blocks the whole failsafe suite. CI will be its first execution.