Skip to content

test(events): prove an event on the maximum is rejected after the run-context append [IFC-3008] - #10249

Draft
ogenstad wants to merge 63 commits into
developfrom
pog-event-budget-rejection-control-IFC-3008
Draft

test(events): prove an event on the maximum is rejected after the run-context append [IFC-3008]#10249
ogenstad wants to merge 63 commits into
developfrom
pog-event-budget-rejection-control-IFC-3008

Conversation

@ogenstad

@ogenstad ogenstad commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Why

Follow-up to #10242, taking a review suggestion from @polmichel that arrived after it merged.

That PR covered the related-resource budget from one side only: an event built on the budget still
validates after Prefect's events worker enlarges it in place. On its own that assertion would pass
just as well against a mechanism that never rejects anything, so it does not demonstrate that the
reservation earns its place.

Goal: pair it with the control that shows the failure being prevented is real.

Non-goals: no production code changes, and no revisiting of how the reservation is sized.

What changed

One new test in backend/tests/unit/event/test_limits.py. An event built on the maximum rather
than the budget has no room for the in-place run-context append, so the enlarged event lands above
the limit and no longer validates:

assert len(event.related) > get_prefect_max_related_resources()
with pytest.raises(ValidationError, match=rf"The maximum number of related resources is {case.configured_max}"):
    Event.model_validate(event.model_dump())

It reuses SURVIVAL_CASES, so it runs at 500 / 200 / 100 - the proportional branch of the
reservation, the point where the two are equal, and the floor branch - matching its companion.

The match is pinned to the case's configured maximum rather than \d+, per
.agents/rules/testing-python.md: match a substring only where the variable part genuinely cannot
be pinned down, and here it can.

How to review

The two tests are deliberately near-identical - they differ only in the size the event is built at
and in whether validation is expected to succeed. That symmetry is the point, so I left the ~18
duplicated lines rather than extracting a builder. Say the word if you would rather see the shared
construction pulled into a helper.

How to test

uv run pytest backend/tests/unit/event/test_limits.py

15 tests, all passing. invoke format and invoke lint (ruff, ty, mypy) clean.

Impact & rollout

  • Backward compatibility: n/a, test-only.
  • Performance: n/a.
  • Config/env changes: none.
  • Deployment notes: none.

Checklist

  • Tests added/updated
  • Changelog entry added - n/a, test-only change, labelled ci/skip-changelog
  • External docs updated - n/a
  • Internal .md docs updated - n/a, the mechanism is already documented in
    dev/knowledge/backend/events.md
  • I have reviewed AI generated content

Review in cubic

ogenstad and others added 30 commits August 7, 2026 14:06
GroupMutatedEvent.get_related() built an unbounded related-resources
list (2 entries per member, 3 per ancestor), so a single mutation
changing a few hundred members exceeded the Prefect maximum and the
whole event was silently dropped: no activity log, no membership
automations.

Members and ancestors are now a single entry each (dropping the
duplicate related.node and the dead group.update roles) and the list is
truncated at the Prefect maximum, so the event is always recorded. The
event query filter and related-nodes output read all three related-node
roles and dedupe by id, keeping output identical across the consolidated
format and older events still in Prefect retention.

Fixes #10127
The resource loop in sort_key wrote the resource priority to type_cost
instead of item_cost. Every item carries both a type marker and a
resource marker, so that loop always ran and always erased the type
priority the first loop had computed.

Items were therefore sorted by resource kind only, and an integration
test could run before the smoke test of the same resource. Nothing
covered the ordering, so add unit tests on the collection hook.
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](dorny/paths-filter@v4.0.2...v4.0.3)

---
updated-dependencies:
- dependency-name: dorny/paths-filter
  dependency-version: 4.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
…le/dorny/paths-filter-4.0.3

chore(deps): bump dorny/paths-filter from 4.0.2 to 4.0.3
The "Example:" preview and the (i) source tooltip in the preferences
forms formatted through an ad-hoc date-fns format() call that took no
timezone, so they showed browser-local time — off by the zone offset,
and at some hours by a whole calendar day (with ISO_8601 they also
stated a false offset). The duplicate formatter is deleted; previews now
render through the shared preference-aware mechanism, exposed as
formatWithPreferences, using the unsaved timezone the form currently
holds. The user form falls back to the effective inherited zone, while
the global editor falls back to the browser zone, since an unset global
timezone means "browser default" for every viewer.

Fixes #10175

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ative

DateDisplay's compact/relative heuristic compared the day difference
with a signed value, so any future date — however distant — fell through
to the relative branch and rendered as "in 4 years" instead of the
user's preferred date format. The window is now a week either side of
now, keeping the relative phrase where it reads best and the preferred
date beyond.

Fixes #10173

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The object detail page's DateTime attribute renderer entered
DateDisplay's metadata-oriented age heuristic, so user-authored
timestamps collapsed to a bare date (or a relative phrase) while the
list view showed the full preferred datetime. The detail renderer now
opts out with fullTimestamp, matching the table cell.

Fixes #10172

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4 to 5.
- [Release notes](https://github.com/codspeedhq/action/releases)
- [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md)
- [Commits](CodSpeedHQ/action@v4...v5)

---
updated-dependencies:
- dependency-name: CodSpeedHQ/action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/stale](https://github.com/actions/stale) from 10 to 11.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](actions/stale@v10...v11)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '11'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
… cleared

The date-format example fell back to the caller's effective timezone while
the form's timezone field was empty. When that effective value came from
the caller's own override, clearing the override previewed the zone being
removed rather than the one about to apply. The fallback is now the
inherited zone, expressed as a domain rule: a USER source resolves to
null, since the API resolves the inherited value away once an override
wins and the browser zone is the only honest stand-in.

Also trims the comments added across these fixes down to the load-bearing
rationale, per the repo's code-documentation style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…preview

inheritedTimezone excluded a USER source but let any other source through,
so a DEFAULT source carrying a value would have been reported as inherited
— contradicting the pattern resolver, which discards a DEFAULT value even
when one is present. Only GLOBAL is an inherited layer, so whitelist it.

No behaviour change against today's API, which always pairs DEFAULT with a
null value; this keeps the two rules consistent if that ever loosens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A worker without a local copy of a repository clones it on demand. That path only carries the repository id and name, so the repository object never learns which git ref to track. It fell back to the platform default branch. Git then failed on any repository whose default branch is not `main`, the repository went to error, and artifact generation failed with it.

It takes more than one worker to see this. Each worker keeps its own git directory, so only the worker that added the repository has the clone.

`InfrahubRepositoryBase` gets one abstract method, `resolve_checkout_ref`. The re-clone path calls it and passes the result to `create_locally`. `CoreRepository` reads `default_branch`, `CoreReadOnlyRepository` reads `ref`. The lookup only runs when the clone is missing, so an existing clone costs nothing extra.

Fixes #8749
Both branch-details suites assert that no "default" text is visible on a
non-default branch, to prove the default badge is absent. The assertion
used a substring match, so renaming the attribute label to "Schema
differs from default branch" made it match that label instead and the
tests failed for a page that is rendering correctly. The positive
assertion was updated with the rename; this negative one was missed.

Matching exactly still catches a stray badge, whose text is only
"default", without tripping on prose that contains the word.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(backend): delete branch data in bounded batches

Deleting a large branch ran as a single Cypher statement whose peak
transaction memory scaled with the size of the branch: it collected the
element id of every vertex touched by a deleted edge into two lists and
concatenated them. On a big enough branch that exceeded
dbms.memory.transaction.total.max, and because the inner writes committed
in batches the failure left the branch stranded in DELETING -- invisible
in the branch list -- with most of its data still in the graph.

Replace it with a set of bounded queries driven from Python by a new
BranchDeleter component: the agnostic peers of branch-only nodes first,
then one batch of edges per relationship type until none are left. Peak
transaction memory is now a function of the batch size rather than the
branch, and naming the relationship type lets the branch range index
serve the match instead of scanning every edge in the database.

Each batch also deletes the vertices its edge deletions left bare. That
is only sound because every branch edge is removed by the batch's DELETE
and both endpoints are re-examined afterwards, so the batch that removes
a vertex's last edge is the one that sees it at degree zero. A DETACH
DELETE would break it by removing edges that never reach a batch of their
own, stranding the vertices on their far side.

Branch.delete now raises instead of silently dropping only the Branch
vertex; callers use BranchDeleter.

On a branch of 1,074,217 edges this deletes the same 416,021 vertices as
before, in 27.8s rather than 87.9s, and completes at a 512 MiB
transaction limit where the previous implementation ran out of memory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): finish deleting branches abandoned in DELETING

A branch delete that ran out of transaction memory committed part of its
work before failing, leaving the branch with the DELETING status and the
rest of its data in the graph. That state was unreachable: the branch is
filtered out of the branch list, and Branch.get_by_name hides it by
default, so the delete could not be retried and the space could not be
reclaimed.

Add graph migration 075, which finds every branch still in DELETING and
runs it through BranchDeleter, then removes the branch node. It reuses the
normal delete path rather than reimplementing it, loading each branch via
the ignore_deleting escape hatch. Finding them needs a dedicated query
because the shared branch list query filters DELETING out
unconditionally.

BranchDeleter now returns the number of edges it removed so the migration
can report progress on the migration console. `infrahub db migrate` raises
the infrahub log level to WARNING, which hides the deleter's own logging,
and deleting a large branch takes long enough that silence looks like a
hung upgrade.

The migration is a no-op where no branch is in DELETING, and safe to
re-run: an interrupted delete resumes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): allow retrying a branch delete left in DELETING

A delete that failed part way through set the DELETING status before doing
any cleanup, and both the BranchDelete mutation and the branch-delete flow
looked the branch up with the default lookup, which hides that status. The
retry reported the branch as missing, so the only way to reclaim the data
was the upgrade migration -- which runs once, leaving any later failure
stranded for good. Both lookups now pass ignore_deleting=False.

Two consequences worth knowing: the mutation will accept a delete for a
branch whose delete is still running, and nothing serialises the two runs
(each query involved is idempotent); and a retry re-emits
BranchDeletedEvent. Making the surrounding flow steps idempotent is left
for later.

Cap the agnostic cleanup at 500 rows rather than query_size_limit. Those
batches count Nodes, and each one can drag an unbounded number of peer
vertices into the transaction with it, unlike the edge batches where one
row is one edge. 500 is what this phase used before the batching work, and
raising it to 5000 was incidental rather than deliberate.

Drop the edge accounting comment claiming the vertex cleanup can remove
extra edges. That was true while the cleanup used DETACH DELETE; it now
only deletes vertices that are already bare.

Add the missing tests: Branch.delete refusing, the default/global guards
that moved onto BranchDeleter, and retrying a branch left in DELETING.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(backend): own the branch in the delete-retry test

The test created the branch with `create_branch`, which leaves created_by
as the system user, so the BranchDelete mutation took its permission
branch and the assertion depended on how a super-admin grant resolves
against a specific DELETE_BRANCH check. That resolved differently in CI
and the test failed with PermissionDeniedError.

Use the existing first_account / session_first_account fixtures and make
that account the branch owner, so created_by matches the requesting
account, the permission check is skipped, and the result turns on the
DELETING status alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): make the DELETING branch cleanup migration robust

Address review findings on migration 075 and the deleter's accounting.

The migration's branch lookup suppressed the generated pagination. A read
query with no limit of its own is executed page by page, so with the
SKIP/LIMIT suppressed every page re-read the whole set and the paging
never reached a short page: with at least query_size_limit stranded
branches the upgrade would never finish. Pagination is enabled again and
the lookup orders by branch name, which is unique, so the pages are
disjoint.

Each branch is now deleted in its own try. A failure is reported as
"branch '<name>': <error>" and the loop continues, so an operator gets the
names of everything that still needs a re-run instead of the first
exception and an unknown remainder. Failing to list the branches at all
still aborts, since there is then nothing to iterate.

Cap the agnostic cleanup at min(batch_size, 500) rather than a fixed 500,
so lowering the configured batch size to fit a constrained database is not
answered with a larger batch than was asked for.

Count the agnostic cleanup's edges towards the total the deleter reports.
It detaches peer vertices, so leaving it out made the migration's
per-branch progress undercount for any branch with agnostic data.

The 075 test asserted the stalled branch's node was unreachable from main,
which it always was -- it only ever existed on that branch, so the
assertion held whether or not the migration ran. It now reads the node on
its own branch, before and after. A third case covers one branch of three
failing, using a FailingBranchDeleter that delegates to the real deleter
for the others so the test proves they were reclaimed rather than merely
attempted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(backend): only run post-delete work when this attempt removed the branch

BranchDeleter.delete returned the number of edges it removed, which only
the upgrade migration used, for its progress output. It said nothing about
whether this attempt was the one that removed the branch -- so now that a
delete can be retried, two attempts on the same branch could both go on to
cancel the proposed changes, emit BranchDeletedEvent and delete the Git
branch.

Return a BranchDeleteResult carrying both branch_deleted and
edges_removed. Removing the vertex is itself the claim: two attempts are
serialised on it, so exactly one reports nodes_deleted, with no window of
the kind a read followed by a delete would leave. The branch-delete flow
returns early when it did not make the claim, and the migration reports
whether the branch was still there.

Skip the DELETING status write when the status is already set. It is a
wasted query for a branch whose earlier delete failed part way through,
and it fails outright if the branch has meanwhile been removed -- which
made delete() unsafe to call twice at all, whatever it returned.

A narrower window remains: an attempt that has not yet written the status,
and whose write lands after another attempt removed the vertex, still
raises rather than reporting false. It fails the run instead of
double-processing, and closing it properly wants a branch-scoped lock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* make sure branch is deleted from git during concurrent deletes

* move delete_branch flow logic to a new component for easier testing

* formatting

* add default/global branch delete guard at higher level

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
update Branch.get_by_name to include ignore_deleting=True so that a
delete can be retried
pass the "low_context" into the branch delete orchestrator for follow-up
tasks
Keep stable's behavior from #10195 on top of the 1.11 frontend architecture:
git-repository.tsx links to the node's concrete __typename using the moved
NodeCore/object-urls modules, and the profiles-field test is ported onto
getObjectForEditingFromApi, which replaced generateObjectEditFormQuery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…C-3001]

The git agent relied on the SDK client default for sync_with_git when creating
an imported branch in the graph. That default flipped from True to False in the
SDK, so branches discovered on a repository remote were created without the
flag.

Merging such a branch, running its repository checks and generating its
artifacts are all gated on sync_with_git, so the git side of a merge was
silently skipped: the git branch was never merged into the repository default
branch and the recorded commit never advanced.

Closes #10208
…d-run

A dependency discovered after Vite's initial scan triggers a re-optimization
reload that resets vi.mock, which surfaces as "mockClear is not a function"
in whichever spec happens to be running. CI hit it on two different files
across consecutive runs. @radix-ui/react-dropdown-menu is a declared app
dependency the scan misses, so pre-bundling it removes that trigger.

Two entries already in this list, @dagrejs/dagre and html-to-image, still
reload: they belong to a workspace package consumed as source and do not
resolve from the app, so pre-bundling silently skips them. Fixing that needs
them declared here, which is a dependency change of its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(core): cap and consolidate group mutation event related resources
polmichel and others added 24 commits August 11, 2026 16:54
…10215)

BusRecorder is a recording double only; its reply/rpc raises are
should-not-be-called guards, not a failing double. Present the recording
and failing doubles as the two patterns to write, with BusRecorder as the
recording example, rather than claiming BusRecorder illustrates both.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…--file (#10217)

In --file mode, `infrahubctl object update` still requires KIND and
IDENTIFIER as positional arguments even though their values are ignored and
the file supplies the targets. State both facts so a reader doesn't assume
the positionals can be omitted.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(rebase): use the branch-creation schema as the migration baseline

This is a backport of the behaviour introduced upstream in fd31b14,
which arrived inside a MergeSchemaAnalyzer refactor that does not apply
to stable. Only the behaviour is ported, not the refactor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(rebase): restore the branch's own schema when a rebase rolls back

SchemaUpdateCoordinator used a single origin_schema for two unrelated
jobs: the baseline the migrations compare against, and the schema
restored into the registry when the update fails. Those coincide for a
plain schema update, but not for a rebase, where the baseline has to be
the common ancestor. A rebase that failed during its migrations
therefore rolled the branch back to the schema it was created from,
silently dropping any schema change made on the branch and persisting a
wrong schema hash with it.

Split the parameter into migration_baseline_schema and rollback_schema,
both required so the two roles cannot be conflated again. The three call
sites where they genuinely coincide pass the same value twice; only the
rebase differs, capturing the branch's registry schema before the graph
is rebased.

The graph side already behaves: RollbackQuery reverses edges stamped
with the unified timestamp, and the rebase shares that timestamp, so the
branch's data is back to its pre-rebase state and the branch's own
schema is the consistent thing to pair with it.

Covers both schemas the coordinator is handed in one component test,
driving the real rebase flow twice, once succeeding to observe the
migration baseline and once failing to observe the rollback. The two
cases share the fork-before-inheritance setup, which dominates the
runtime. WorkflowRecorder gains an execute_results hook so a test can
make a workflow report errors without patching.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* only get the schema from the database when necessary

* update SchemaUpdateCoordinator docstring

* verify hash is rolled back during failed rebase

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: release 1.10.7

* add another commit and update changelog/release notes

---------

Co-authored-by: Patrick Ogenstad <patrick@ogenstad.com>
Keep release-1.11's refactored SchemaUpdateCoordinator (GraphRollbacker,
timestamp-scoped rollback) and the branch merge orchestrator in place of
stable's pre-refactor shape, and forward-port the half of the rebase
backport that release-1.11 did not already cover: the schema restored into
the registry on failure is now passed separately from the migration
baseline, so a failed rebase puts back the schema the branch itself had
rather than the common ancestor it migrated against. rollback_schema is
required at every call site.

WorkflowRecorder keeps the unified calls list and gains the canned
execute_results the incoming rebase test relies on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nt package

The file merged cleanly from stable but kept importing PrefectEventData and
InfrahubEventFilter from the modules that the event package split replaced,
so nothing resolved at import time. Caught by ty, which resolves imports
where ruff and the mypy configuration do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stable to release 1.11 (w/ resolved conflicts)
The Marketplace docs page only linked to the live marketplace once,
inside the fetch instructions. Link it from the intro, point the
find-schema step at the browse catalog, link the example identifiers
to their detail pages, and list the marketplace under related
resources.

Co-authored-by: Yvonne Jouffrault <yvonne@opsmill.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Merge stable into release-1.11
…nd (#10180)

* fix: advance number pool past values already present on the target kind

A CoreNumberPool derived its next value only from its own reservations, so a
value already present on the target kind (created directly or brought in by a
brownfield import) was invisible. The pool offered it, the uniqueness
constraint rejected the save, and since the failed allocation reserved
nothing the pool re-offered the same value on every attempt, parking
permanently at that value.

Allocation now excludes values already held on the target kind, matching the
uniqueness-constraint validator's visibility (is_isolated=False, deletions),
and only for globally unique attributes so per-relationship and non-unique
attributes stay fully allocatable.

closes #10179

* refactor: address review on number pool taken-value query

- Drop the toInteger cast on the range predicate so it can use the value
  index (av.value is a native integer, as NumberPoolGetAllocated relies on).
- Add WITH DISTINCT n, attr, av before the CALL so it runs once per
  (n, attr, av) instead of once per edge pair.
- Condense the guard comment.
- Remove an em dash from the changelog fragment.

* refactor: group number pool taken-value query by attribute and dedupe tests

Group the taken-value lookup by (n, attr) and return the latest active
value per attribute, dropping the unused ha/hv bindings and re-applying the
range filter after the subquery. Extract the shared ticket-schema and pool
setup into a fixture.

* test: load number pool schema once via class-based allocation tests

Group the three shared-setup allocation tests into a class whose class-scoped
fixture loads the ticket schema and pool a single time, instead of reloading
per test. The methods run in definition order and build on shared data.
#10234)

* fix: forbid unknown fields in the exported node JSON Schema [INFP-234]

SchemaLoadAPI now derives from the generated write models, which set
extra="ignore" because unknown-field policy is applied imperatively when a
schema is loaded. The exporter was never revisited, so the published document
lost every additionalProperties: false it used to inherit from the internal
models' extra="forbid", and an editor validating against it accepted a typo
the load endpoint rejects.

Close every object in the exported document, and declare the read-only fields
alongside it marked deprecated rather than omitting them: the load endpoint
accepts one, drops the value and reports a warning, so a closed document that
left them out would turn a schema read back from Infrahub into a file full of
errors. The read-only names come from the generated contract table, the same
one the load endpoint validates against, so the document cannot drift from the
endpoint's verdict.

Also drop AttributeSchema.model_json_schema. It set out to bind a kind to its
parameters for the language server but never reached the published document:
pydantic does not call a nested model's classmethod when building the parent,
and its refs were Draft-7 style that would not resolve in a $defs document.
The write models' discriminated union does that job now. Regenerating
openapi.json after removing it produces no diff.

* fix: annotate the validator fixture against the jsonschema protocol

* test: assert the invariant hardening relies on, and pin each rejection reason

Hardening reaches the root and $defs only, which covers the document while
pydantic hoists every nested model. Nothing stated that, so a field typed as a
mapping would render an inline object the sweep never sees. Assert it instead:
such a field now fails the suite and gets a deliberate decision, rather than
either leaving one spot open or having its value schema overwritten by a
recursive sweep.

Every case now pins the message the document must report, so a case cannot go
green by failing for an unrelated reason. Drop the escape clause that excused a
required read-only field from carrying the deprecation marker: no field reaches
it, and it could only ever weaken the assertion. Count what each structural
test asserted so an empty document cannot pass it vacuously.

Adopt the project's dataclass test-case shape: field docstrings, a typed
module-level constant, and IDs that name the expected outcome.

* chore: declare jsonschema for tests and build the document without mutating

The schema tests validate against jsonschema, which reached the environment only
as a transitive dependency, so a change upstream could have removed it. Declare
it in the dev group with a floor and no ceiling: the floor is the version whose
validation messages the tests assert verbatim, and leaving the ceiling off keeps
this pin from ever blocking an upgrade that wants a newer one.

Build the hardened document from a copy rather than editing the one handed in. A
function named for building a document should not leave the caller's own changed
underneath it, and a test now holds that contract.
* reorganize learn folder

* Move everything to learn

* Cleanup - improvements - links

* docs: rework Infrahub Labs hub per review — value-first intro, topic grouping, text links

- Rewrite the labs overview: lead with what labs are for and what you gain,
  add when-to-use guidance, group standalone labs by topic (getting started
  vs advanced) with time estimates and when/why for each lab
- Rename page and sidebar label from Labs to Infrahub Labs
- Replace StandoutLink buttons with linked text at the end of each paragraph;
  remove the now-unused StandoutLink component
- Say lab instead of tutorial on lab pages, now that Tutorials is a separate
  section

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* various fixes

* Keep transformation naming consistent

---------

Co-authored-by: Yvonne Jouffrault <yvonne@opsmill.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* docs(objects): add "Create objects" overview page

Introduces a single entry point listing every way to create objects
(web interface, infrahubctl, YAML object files, Python SDK, GraphQL
API, Generators, Infrahub Sync) and links it from the related pages.
Also drops the redundant "What you can do with objects" list from
overview.mdx and shortens a couple of page titles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(objects): give Create objects real per-method context

The page shipped as a routing table: it named each method and linked
out, but gave no basis for choosing and showed no example of any
method. It also stated the web UI button as "Create <Kind>" when the
button reads "Add <Kind>".

Each method now carries a paragraph on when to use it, and one running
example (InfraDevice atl1-edge1) is shown four ways through the shared
groupId="method" tabs, so a reader's interface choice persists across
the docs site. Generators and Infrahub Sync move out of the method
table into their own section, since they create objects from a
workflow rather than from a command. Adds sections on Object
Templates, Profiles, and resource pools, and on what happens after an
object is created.

Restores outbound navigation on the Objects hub as "## In this
section", matching the format used by the IPAM and Schema hubs. The
PR had removed the hub's only links to its own child pages.

Facts verified against models/base/dcim.yml (required attributes and
the site relationship), object-create-form-trigger.tsx (button and
panel labels), generators/overview.mdx (execution model), and the
Python SDK and Infrahub Sync docs.

* docs(objects): restructure Create objects into three categories

The page presented seven creation methods in three formats across four
locations: five in a table, four of those repeated as tabs, one as its
own section, and two more introduced only as intro bullets and then
described again below. Nothing shared a shape, so the methods could
not be compared.

Groups them under what drives the create — directly, from a file, from
a workflow — and gives every method the same three beats: when to use
it, an example or the mechanism, and where to read more. Drops the
table, since the useful content per method is a descriptive paragraph
rather than a cell value, and three categories are easier to hold than
seven rows. Drops the duplicated intro bullets.

Folds Object Templates, Profiles, and resource pools into "After an
object is created", framed as where an attribute's value came from,
which ties them to the metadata and lineage they are recorded in.

Follows the structure Kubernetes uses for the equivalent page
(Kubernetes Object Management), where parallel treatment of each
technique is what makes them comparable.

* docs(objects): fix voice regressions in Create objects

Words removed earlier had returned during the restructure. Fixes each
against the rules they broke:

Figurative usage: "a file that lives in Git" -> "stored in Git";
"what drives it" -> "what starts the work"; "three commands drive it"
-> "run it"; "objects follow from data" -> "are determined by data";
"circuits a service definition implies" -> "required by"; "a Group
naming the objects" -> "that lists".

Vague UI jargon: "appear as selectable controls" -> "pick an Object
Template, assign Profiles, and allocate from a resource pool", naming
what the reader does.

Features as the agent, in the section flagged for exactly this:
"Object Templates supply structure" -> "Pick an Object Template to
start the object with...". The reader is now the subject of all three.

Definition by negation: "without leaving the terminal" and "without
changes" removed. Unverifiable claims: "the quickest way", "actually
needs", "straightforward scripting". Overclaim: "Infrahub has four
interfaces" -> "Four interfaces create objects", since the REST API
exists too.

Splits the four Generator trigger conditions out of one long sentence
into a list.

* docs(objects): restore the category summary at the top

The restructure compressed the three categories into a single
sentence, so a reader had no way to see what each one is for before
scrolling into it.

Restores a three-item summary, each naming what the category is and
when to use it, linking to its section so a reader can go straight to
the one they need.

Reworks the "from a workflow" opener so it draws the distinction
between the two tools rather than repeating the summary: a Generator
works from data already in Infrahub, Infrahub Sync from data in
another system.

* docs(objects): make the category summary tell them apart

"Use this when you are deciding what to create as you go" gave a
reader nothing to choose on, and the three descriptions were not
comparable to each other.

Each now states the same three things, so they can be read against one
another: how many objects at a time, where the definition comes from,
and whether it happens once or keeps happening. The lead-in names
those axes instead of "what starts the work".

One at a time, manually or in a short script -> directly. Many
declared together and reviewed in Git -> from a file. Created and
updated as the source changes, without anyone issuing a command ->
from a workflow.

* docs(objects): make each section opener say what it is for

Applies the test used on the category summary to every section and tab
opener: say what the thing is for, be concrete, give the reader
something to decide on.

Object files led with "data that changes rarely", which describes a
property rather than a purpose. Now states what the file is and what
keeping it in Git buys: rebuild an instance from it, promote the same
dataset from development to production, review a data change in a
pull request. Names the data it suits — sites, roles, platforms,
device types, tags.

GraphQL was defined by negation ("when the client is not Python").
Now positive and concrete: any language or tool that can send an HTTP
request, with examples.

"Create objects directly" said the four interfaces are equivalent but
not what separates them; it now names the four contexts. The web
interface opener explains why starting there helps — the form shows
what a script will need to supply later. The workflow opener states
what both tools are for rather than only how they run.

Promotion between environments verified against
git-integration/multi-environment.mdx.

* docs(objects): fix three awkward openers

"Four interfaces create objects" made the interface the actor, the
same problem flagged earlier with features. Now "There are four ways
to create an object one at a time", and the abstract noun is gone.

"How you create objects depends on..." opened on a nominalization
instead of the fact the reader wants. Now leads with "There are three
ways to add objects to Infrahub", then the criteria for choosing.

The workflow opener started on "Both", referring to nothing yet
introduced in the section, and ended in "That is what suits them to
data no one should be maintaining manually — the objects that ought to
exist because something else is true", which said very little at
length. Replaced with the actual use case: some objects exist because
other data does and have to stay correct as it changes, so maintaining
them manually means repeating the work every time.

Uses "choose" rather than "pick" throughout.

* docs(objects): say what applies to which creation methods

"All three work whichever method created the object" left both halves
unclear: "work" stated nothing, and the clause did not say what the
methods were. Names the subjects and points back to the page's own
three categories, so the scope is explicit.

Also drops the negation in the lead-in — "so that you do not fill it
in each time" becomes "so the object receives it on creation".

* fix linting

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Yvonne Jouffrault <yvonne@opsmill.com>
…10237)

The bulk recompute dispatcher tagged its run as a database change once it had
values to write. add_tags rebuilds the whole tag list from the runtime snapshot
taken when the run started, and a tag update never refreshes that snapshot, so
this second call dropped the branch tag and every related-node tag the flow had
added at its start. A recompute task then fell out of the task list filtered by
branch and out of the task list of a node, which is why the tasks looked like
they vanished one by one.

The call was redundant anyway. Every flow that reaches the dispatcher already
declares the database-change tag on its workflow definition, and Prefect merges
the deployment tags into the run when it creates it. Drop the call.
* fix: narrow Python transform closure to its own file (closes #9644)

A Python transform's and a generator's dependency closure was every
git-tracked file in the directory holding its file_path. Sharing a directory
between definitions - each sitting next to its own query and helper modules -
therefore regenerated every artifact rooted there on any single-file edit,
including edits to files the definition never used.

Auto-detection now claims the entry file alone. Files a source genuinely
depends on are declared through watch.files, where naming the containing
directory restores the previous closure for that definition. A definition
that has not declared watch still folds the commit id into its fingerprint,
so no safety net is lost.

* docs: fix release-note style and correct closure wording from review

Vale enforces the branded plural over changelog fragments, which the
release-note style job checks and the fragment failed. The docs style guide
also forbids "transform" as a noun, which Vale only catches in the plural, so
the singular slips in the fragment and in python.mdx are normalised too.

The knowledge note claimed the untrusted-closure fallback was effectively dead
for Python. A watch.files pathspec git cannot enumerate raises GitCommandError,
which the aggregator isolates to complete=False, so both routes are now named.

The component selection tests hand-set their stored closures rather than
declaring watch, so their comments no longer credit watch.files for what the
fixtures hardcode, and the two renamed tests state what they actually cover:
the gate treats every closure member alike.

* docs: attribute Python regeneration to the fingerprint, not an incomplete closure

An undeclared Python Transformation was described under the incomplete-closure
fallback, which is the wrong mechanism: its closure is always complete, and the
regeneration comes from the fingerprint being tied to the current commit while
no watch declaration exists. Three pages claimed the closure was incomplete or
that declaring watch marks it complete; an empty files list never reaches the
closure union at all.

* docs: keep closure-completeness claims in one place

The user-facing pages restated an internal invariant in their own words, which
is how three review rounds each caught a different overstatement of it. They now
describe only what a reader acts on, and mention dependencies_complete solely
for Jinja2, where auto-detection really does report it. The knowledge note is
the single place that spells out when a Python closure can be incomplete, and it
now scopes that to misconfiguration rather than claiming the fallback is dead.
Merge stable into release-1.11
…hint (closes #10174) (#10198)

* test: add failing test for 10174-timezone-preference-validation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: validate preference timezone at write and correct the UI source hint

Reject a non-IANA timezone when setting a user or global preference, so an
unusable zone can no longer be persisted through the API/SDK. Validation runs
at the write path only (resolving the value against the runtime zone database);
the model stays lenient so a bad value stored earlier still reads back, and an
empty value normalizes to unset so the write reply agrees with later reads.

Correct the preferences screen so it no longer claims a stored zone is in effect
when the viewer's browser cannot render it; the hint now reports the browser
fallback instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: reject implementation-defined timezone keys at write

A full system zone tree resolves entries like localtime, posix/*, and right/*
that browsers reject and that no client should store as a preference. Reject
them explicitly before construction so the guarantee holds regardless of which
zone database the runtime ships (verified: the slim runtime resolves localtime).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: lock in construction-based acceptance of a canonical offset zone

Guards against a future regression to an enumerated allowlist, which would
reject a resolvable zone the runtime can apply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover the GLOBAL-source unrenderable timezone hint

The corrected hint fires for an org-default zone the browser cannot render, not
only a user override.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: reject the posixrules pseudo-zone at write

posixrules resolves in the runtime zone tree but is not a browser-renderable
IANA zone; add it to the rejected keys alongside localtime and posix/right.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: assert the exact error message when rejecting a non-IANA timezone

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: rename the timezone preference validator to validate_timezone

The function validates by construction and returns the value unchanged, so the
name now reflects that it validates rather than normalizes. Trims the docstring
and notes the accepted set is runtime-dependent, best confirmed live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…um [IFC-3008] (#10242)

* fix(core): reserve headroom below the Prefect related-resources maximum [IFC-3008]

Node and group mutation events truncated their related resources to exactly the
configured Prefect maximum. Prefect's events worker then extends that list in
place with run-context resources - flow run, task run, flow, deployment, work
queue, work pool and one per flow-run tag - which skips the client-side
validation, so the enlarged event arrives above the maximum. The Prefect API
answers by closing the /events/in websocket rather than by dropping the single
event.

Both call sites now truncate to get_related_resource_budget(), which reserves a
tenth of the maximum with a floor of 20. get_prefect_max_related_resources()
keeps returning the raw maximum and stays the base for the submission chunk
size, which is a different constraint.

* fix(core): tie the run-context headroom to a single declared worst case [IFC-3008]

The headroom floor and the worst-case append size the tests exercised were two
independent numbers in two files, so raising one would leave the other behind
and silently reinstate the overflow. MAX_RUN_CONTEXT_RESOURCES now declares the
append size in one place, the reservation floor derives from it and the tests
import it.

The survival check also only ran at a maximum where the proportional
reservation binds, leaving the floor untested. It now spans both sides of the
reservation.

Also name the task run in the changelog, matching the docstring.
…-context append [IFC-3008]

The related-resource budget was covered only from the positive side: an event
built on the budget still validates once Prefect has enlarged it. That passes
just as well against a mechanism that never rejects anything, so it does not
show the reservation earns its place.

Add the matching control. An event built on the maximum instead has no room for
the in-place append, lands above the limit and is refused, which is the failure
the budget exists to prevent. Both run over the same maximums, so the pair holds
on either side of the reservation.
@ogenstad ogenstad added the ci/skip-changelog Don't include this PR in the changelog label Aug 13, 2026
@github-actions github-actions Bot added the group/backend Issue related to the backend (API Server, Git Agent) label Aug 13, 2026
@codspeed-hq

codspeed-hq Bot commented Aug 13, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 13 untouched benchmarks


Comparing pog-event-budget-rejection-control-IFC-3008 (e330c24) with release-1.11 (a95933f)

Open in CodSpeed

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file

Confidence score: 5/5

  • In backend/tests/unit/event/test_limits.py, duplicating most of test_event_on_the_budget_survives_the_prefect_run_context_append creates maintenance risk: future changes may update one test path but not the other, causing inconsistent coverage and harder debugging—factor the shared setup/assertions into a helper or parameterized test to keep behavior aligned.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/tests/unit/event/test_limits.py">

<violation number="1" location="backend/tests/unit/event/test_limits.py:107">
P3: This new test duplicates nearly the entire body of `test_event_on_the_budget_survives_the_prefect_run_context_append` (both event-construction list, the `event.related +=` append, and the trailing len/validation logic). The only real differences are `get_prefect_max_related_resources()` vs `get_related_resource_budget()` as the build size and whether validation is expected to raise. Keeping the ~35 duplicated lines in sync means a change to the append or event shape must be applied twice, and the two tests drift apart silently. Extract a shared builder/helper that takes the build size, then have each test call it and assert its own outcome.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic



@pytest.mark.parametrize("case", [pytest.param(case, id=case.name) for case in SURVIVAL_CASES])
def test_event_on_the_maximum_is_rejected_after_the_prefect_run_context_append(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This new test duplicates nearly the entire body of test_event_on_the_budget_survives_the_prefect_run_context_append (both event-construction list, the event.related += append, and the trailing len/validation logic). The only real differences are get_prefect_max_related_resources() vs get_related_resource_budget() as the build size and whether validation is expected to raise. Keeping the ~35 duplicated lines in sync means a change to the append or event shape must be applied twice, and the two tests drift apart silently. Extract a shared builder/helper that takes the build size, then have each test call it and assert its own outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/tests/unit/event/test_limits.py, line 107:

<comment>This new test duplicates nearly the entire body of `test_event_on_the_budget_survives_the_prefect_run_context_append` (both event-construction list, the `event.related +=` append, and the trailing len/validation logic). The only real differences are `get_prefect_max_related_resources()` vs `get_related_resource_budget()` as the build size and whether validation is expected to raise. Keeping the ~35 duplicated lines in sync means a change to the append or event shape must be applied twice, and the two tests drift apart silently. Extract a shared builder/helper that takes the build size, then have each test call it and assert its own outcome.</comment>

<file context>
@@ -100,3 +101,36 @@ def test_event_on_the_budget_survives_the_prefect_run_context_append(
+
+
+@pytest.mark.parametrize("case", [pytest.param(case, id=case.name) for case in SURVIVAL_CASES])
+def test_event_on_the_maximum_is_rejected_after_the_prefect_run_context_append(
+    case: SurvivalCase, monkeypatch: pytest.MonkeyPatch
+) -> None:
</file context>

@ogenstad
ogenstad changed the base branch from release-1.11 to develop August 17, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/skip-changelog Don't include this PR in the changelog group/backend Issue related to the backend (API Server, Git Agent)

Projects

None yet

Development

Successfully merging this pull request may close these issues.