Repository navigation
fix: extend the ignored-filter guard and drop eight read-model range bounds - #16
Conversation
RoleEffort's six effort/time roll-ups and Time's spent/remain are supplied by the server, and AssignableEntity already states the rule for that class of field: a constraint on a server-supplied value fails the whole entity rather than the field, so one out-of-range record would abort a whole list() page with a ParseError. These two join models were asserting an invariant the API never promised. Live probing found the constraints dormant rather than firing, since the instance clamps every over-spend at zero, so this removes a latent failure rather than an active one. A consumer running against an instance or version that does not clamp the same way would hit it. Time's project description now states what a custom-activity entry holds. TP populates project from the custom activity's own project when there is no Assignable: over 445 live custom-activity entries, every one carried a non-null project, and on every one it equalled that activity's own. times.upsert keeps its write-side validation, which is where this library's own reasoning puts a range check, and its docstring now says why it is the only one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lections PortfolioEpics, TestPlanRuns, InboundAssignables and OutboundAssignables each expose assignable rows - every one of their /meta descriptions declares an Assignments collection - and each answers a where= on that collection with the unfiltered rows, exactly as the six typed assignable collections do. A caller narrowing one of them by assignee was receiving silently unfiltered results, which is the failure the refusal exists for. None of the four has a typed manager, so the generic entities map is both the only place the refusal can live and the only route a caller has to them. No new resource class is involved, and the set of refused paths is unchanged: this widens the collections covered, not the paths. Each was admitted against an unfiltered control on the same collection, as the declaration's own rule requires. The relation collections needed that comparison bounded to an Id range: they emit one row per relation, so an unfiltered page spends slots on duplicate ids and the page boundary falls elsewhere, which makes a page-to-page comparison misleading. Bounded, the distinct id sets are identical. The filter names a user Id no instance issues, so an honoured filter would return nothing, and each returned every row instead; where=(Id eq <id>) returns exactly one row on each, so where= demonstrably does reach the server there. Assignments.Count fails differently, and neither of the two ways expected: the query parser rejects it with HTTP 400, as it does Comments.Count, so it is not a silently ignored path at all. The refusal is kept rather than narrowed, because a ValueError naming the join-entity route serves a caller better than a 400 whose body names no field, and the reason text now states both shapes so it is true whichever path was written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the pre-push review of the two commits before it. SPEC.md's numeric-range section still carved RoleEffort and Time out of the no-bounds rule - the carve-out those commits remove - so the repository's normative document contradicted its own code. It now states the rule for the join models too, and names the attached-content byte-size fields as the remaining bounded ones. The coverage claim is narrowed to what the evidence closes. Four collections were checked because four were named for checking, not enumerated from the instance, so an untyped Assignable-derived collection absent from the map is unproven rather than cleared, and the TestPlan family is the obvious one left. SPEC.md and the map's own comment both say so now, because a reader of a published library's spec could otherwise take the guard for exhaustive. RoleEffort's docstring no longer calls every numeric field a server-supplied roll-up: the collection is writable per its own /meta, so the accurate statement is that each value arrives from the server on a read, whoever set it there. Nor does it imply a write-side check covers this model - times.upsert is a Time helper, and RoleEffort has no counterpart. Time's project paragraph drops a causal claim the data cannot support. No activity carries time on more than one project, so copying the activity's project and copying the project the entry was filed under are indistinguishable from that evidence. It also notes a narrowed read leaves the reference None. The refusal is now described as one TP will not filter on rather than one it silently ignores, in the Raises: blocks and docstrings a caller actually reads: Assignments.Count is rejected, not ignored. The reason-text test drops its word assertion and keeps the two status codes, which pin the same fact less brittly. CHANGELOG.md gains the entry README promises for every change, covering both directions - the new refusal breaks a caller who was receiving silently unfiltered rows, and the dropped bound removes a ValidationError a caller may have relied on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: man8/targetprocess-py/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (9)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change rejects unsupported ChangesUnsupported Assignments filters
Server-supplied effort and time values
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant Caller
participant entities.list
participant BaseResource.check_where
participant TargetProcess API
Caller->>entities.list: Submit an Assignments filter
entities.list->>BaseResource.check_where: Check the filter path
BaseResource.check_where-->>entities.list: Reject unsupported path
entities.list-->>Caller: Raise ValueError
🚥 Pre-merge checks | ✅ 7✅ Passed checks (7 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
Summary
Three related read-path changes, on one branch because they land in the same two
files. The guard extension closes a silent-wrong-results hole on four
collections; the other two remove a latent
ParseErrorand document a fieldwhose behaviour was unstated.
The work items these close are tracked outside this repository, so no issue
number is referenced here.
Changes
Refuse the ignored
Assignmentsfilter on four more collectionsTargetProcess accepts a
where=on theAssignmentscollection of an assignableand silently ignores it, answering HTTP 200 with unfiltered rows. The library
refuses that filter before any request is sent. Four collections that also expose
assignable rows were uncovered:
PortfolioEpics,TestPlanRuns,InboundAssignables,OutboundAssignables. All four ignore the filter, so allfour are now refused — a caller narrowing one of them by assignee was receiving
silently unfiltered results.
None of the four has a typed manager, so the refusal lives in the generic
entitiesmap, which is both the only place it can be applied and the only routea caller has to them. The set of refused paths is unchanged: this widens the
collections covered, not the paths, and
ASSIGNABLE_IGNORED_FILTER_PATHSstillhas exactly one key.
Each was admitted against an unfiltered control on the same collection, as the
declaration's own admission rule requires. Every one of their
/metadescriptions declares an
Assignmentscollection, alongside the rest of theassignable surface (
AssignedUser,Times,RoleEfforts,Impediments,TeamStates).PortfolioEpicsNextTestPlanRunsId, no duplicatesInboundAssignablesIdrangeOutboundAssignablesIdrangeTwo filter values were used against each: a user Id that exists on the instance,
and one that cannot exist on any. The second is what discriminates — an honoured
filter must return nothing for it, and each collection returned every row
instead.
The two relation collections needed the comparison bounded to an
Idrangerather than page-to-page, and that is worth recording: they emit one row per
relation, so an unfiltered page spends slots on duplicate ids and the page
boundary falls elsewhere. Compared page-to-page they looked non-identical,
which is a paging artefact and not filtering. Bounded, the distinct sets are
exactly equal.
Instrument controls.
where=(Id eq <id>)returns exactly one row on each ofthe four, so
where=demonstrably reaches the server and discriminates there —"the filter was ignored" cannot be confused with "the filter never arrived".
Re-running the already-admitted case on a typed collection reproduced it exactly,
so the probe is known to register this specific failure shape.
The coverage is what has been checked, not an enumeration. The four were
named for checking rather than derived from a walk of the instance's
collections, so an untyped Assignable-derived collection absent from the map is
unproven, not cleared.
SPEC.mdand the map's own comment both say so, andboth name the
TestPlanfamily as the obvious remaining candidate —TestPlanRunis covered,TestPlanis not.Assignments.Countfails differently, and neither of the two ways expected.It is not silently ignored: the query parser rejects it with HTTP 400
(
BadRequest, "Error during parameters parsing.") foreq 0,gt 0andgt -1alike — andComments.Countis rejected identically, so the rejection isgeneric to a collection
Countinwhere=rather than anything aboutAssignments. The refusal is kept rather than narrowed: letting it throughwould surface a 400 whose body names no field, which serves a caller worse than a
ValueErrornaming the join-entity route. The reason text now states bothshapes, so it is true whichever path was written, and a test pins both halves.
The
Raises:blocks and docstrings a caller reads now say TP "will not filteron" the path rather than "silently ignores" it, which the
Countfinding makesinaccurate.
Drop range constraints from eight server-read fields
AssignableEntityalready states the rule for this class of field: the numericfields carry no range constraint, because a constraint on a server-supplied value
fails the whole entity rather than the field — one out-of-range roll-up would
abort a whole
list()page with aParseError.RoleEffort(six fields) andTime(two) were breaking it, andSPEC.md's own Numeric range constraintssection carved those two models out of the rule; that carve-out is corrected
here, since leaving it would have left the normative document contradicting the
code.
The constraints were dormant rather than firing, since the instance clamps every
over-spend at zero, so this removes a latent failure rather than an active one. A
consumer running against an instance or version that does not clamp the same way
would hit it — and as a published package, that consumer is not hypothetical.
times.upsertkeeps its write-side validation unchanged, which is where thislibrary's own reasoning puts a range check. Its
Raises:block previously ended"…(
Time.spent/Time.remaincarryge=0)", which this change makes false, sothat prose now says why the write-side check is the only one.
Say what a custom-activity time entry's
projectholdsTime.projectsaid only that TP derives it from the Assignable — true for awork-item entry, but a
CustomActivityentry has no Assignable at all, andthe docs did not say what
projectthen holds.It is populated. Across 445 live custom-activity entries, every one carried a
non-null
projectwhile itsassignablewas null, and on every one the projectequalled that activity's own project. Both the class docstring and the field
description now state that, and the existing custom-activity test sets and
asserts
Project. The docstring stops short of asserting a mechanism: since noactivity carries time on more than one project, copying the activity's project
and copying the project the entry was filed under are indistinguishable from this
evidence.
A
CustomActivityis project-scoped, so one conditional change did not apply.The same read settled it, and the existing
CustomActivitydocstring claim —"scoped to a
Projectand aUser" — is correct: 46 of 46 activities carryboth a project and a user, and 0 of 41 activities carrying time span more
than one project. The name-repeat half holds too: 46 activities carry 18 distinct
names, and two repeated names span different projects, so
custom_activities.resolve(name)genuinely can meet an ambiguous name and itsAmbiguousMatchErrorrationale is sound. Nothing in the lookup model or theusage docs needed correcting, and
resolve()'s behaviour is untouched either way.Testing
uv run pytest -q— 1450 passing; coverage 99%.plus a forward-the-valid-filter counterpart so the guard is shown not to
over-refuse on those collections.
existing tests that asserted the opposite are inverted rather than deleted.
shapes, so neither half can be dropped silently.
code: restoring
entities.pyand_joins.pyfrom the base commit fails 17 ofthe new assertions, and restoring
base.pyalone fails the reason-text test —18 in total, independently reproduced during review. Every change is
load-bearing.
literals (
Name contains 'Assignments.cs'), lookalike paths(
TeamAssignments.*,Assignment.Id,Owner.Assignments.Id) andAssignedUser.Iduntouched.refusal whose tests mock the request handler, so a cassette would have to
record traffic the library exists to prevent sending. The
Timetest asserts amodel shape from a dict fixture exactly as every other model test does. The
commit that first introduced this guard added no cassette either.
Context
package, and
CHANGELOG.mdrecords both directions. The new refusal raisesValueErrorwhere 0.2.1 returned rows — for a caller who was receivingsilently-wrong rows. Removing a validation constraint only widens what parses,
so nothing previously accepted is now rejected, but a caller relying on
ValidationErrorfor a negative server-supplied value no longer receives one(including on assignment, since the model validates assignment). That is the
point of the change, and the reason the write-side check stays.
ge=0bounds survive onserver-supplied read fields in the attached-content models
(
Attachment.size,UploadedFile.persistedSize,UploadedFile.size), and atest actively asserts one of them fires. The repository therefore holds two
positions on the same class of field. Practical risk is negligible — a
negative byte count, and
UploadedFileis a single-response model rather thana
list()page — but it is outside the eight fields this change covers, so itis named in
SPEC.mdrather than silently changed.SPEC.mdis at its line ceiling. It was 996 lines against a 1000-lineceiling before this change and is 999 now, so the notes there are deliberately
terse and the per-collection evidence lives in the commit messages and this
description instead. Any further prose in that file trips
scripts/check_large_files.py; splitting it along a real seam is separate work._spellings()derives plurals by appendings/es, so the new entriesalso yield spellings TP has no collection for (
testplanrunes). That ispre-existing behaviour — the map already contained
buges,taskesandassignablees— and harmless: none of the spurious spellings collides with areal TargetProcess collection name, so no real collection is falsely refused.
Checklist
uv run ruff check .anduv run ruff format --check .passuv run mypy --strict srcpassesuv run pytest -qpasses and coverage stays at or above 90% (99%)uv run pre-commit run --all-files --hook-stage pre-commitpassesuv run pre-commit run --all-files --hook-stage pre-pushpassesAPI; the one new module-level name is private and commented
TODO/FIXME/HACK/XXXmarkers name an issue (e.g.TODO(#123))🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Documentation