Skip to content

26.7 release-bug batch: api-gateway CORS, AM listing / IdP errors / conversions, authz system-actor#9

Closed
diffora wants to merge 8 commits into
mainfrom
VHP-2193
Closed

26.7 release-bug batch: api-gateway CORS, AM listing / IdP errors / conversions, authz system-actor#9
diffora wants to merge 8 commits into
mainfrom
VHP-2193

Conversation

@diffora

@diffora diffora commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Batch of 26.7 release-bug fixes, one commit per ticket plus a post-review remediation commit.

VHP-2193 — CORS-expose the ETag response header (api-gateway)

Browsers may only read non-safelisted response headers when the server sends Access-Control-Expose-Headers; the gateway sent none, so every ETag-guarded (If-Match) RBAC/AM write was impossible cross-origin. Adds cors.exposed_headers (default ["ETag"]), config-load validation for wildcard+credentials combos (previously a tower-http startup panic), and warnings for unparseable list entries.

VHP-2084 — $orderby=status on the tenant children listing (AM + toolkit-db + resource-group)

Status-column sorting returned 400. Root cause: the cursor codec was bound to the wire String kind while the cursor value is the storage SMALLINT. Adds ODataFieldMapping::cursor_kind (default = wire kind; no existing mapper affected), enables lifecycle-ordinal ordering for status, sweeps resource-group's three type fields carrying the same latent mismatch, and updates the OpenAPI doc.

VHP-2158 — typed IdP uniqueness / password-policy rejections (AM SDK + AM)

IdpUserOperationFailure gains DuplicateUser { Username | Email | UsernameOrEmail } and PasswordPolicy; AM maps them to 409 already_exists on the user resource (stable colliding-field token as resource_name, curated detail derived in one place) and 400 with a password/PASSWORD_POLICY field violation. UsernameOrEmail covers Keycloak's combined ModelDuplicateException constant (the only direct 409 shape on KC 26). The plugin half that emits these lands in vhp-core.

VHP-1729 — soft-delete cancels the pending conversion in-TX (AM)

DELETE /tenants/{id} tombstoned the tenant but left its pending mode-conversion row dangling. schedule_deletion's SERIALIZABLE TX now terminal-resolves it (cancelled_by = deleting actor); the idempotent already-Deleted path runs the same fenced cancel, so pre-fix orphans are healed by retrying the DELETE. Audit event is emitted after commit (no double-count on serialization retry).

VHP-2376 — platform-scope the user-groups cascade system actor (AM)

The retention hard-delete reaper's RG cascade ran scoped to the tenant being deleted; the resolver's subtree grant rooted at a Deleted tenant materializes to zero and fails closed, deferring the whole backlog forever. The cascade context is now platform-scoped (nil tenant → Global at the resolver); resolver-side halves land in vhp-core (vp-authz-resolver-plugin).

Verification

cargo test / clippy --all-targets / fmt --check green on the five touched crates (toolkit-db, api-gateway, account-management, account-management-sdk, resource-group): 1789+ tests. An 8-angle review with verifier passes ran over the branch; all confirmed findings are remediated in the final commit (KC combined-constant handling verified against Keycloak 20/26 sources).

Coupling

vhp-core consumes this via the submodule pin: its VHP-2158 branch (vp-idp-plugin classification + authz-resolver fix) needs the pin bumped to this branch's head after merge.

Summary by CodeRabbit

  • New Features
    • Added clearer account-provisioning errors for duplicate users and password-policy rejections.
    • Tenant lists now support sorting and cursor pagination by lifecycle status.
    • Pending conversions are automatically cancelled when a tenant is deleted.
    • CORS responses now expose configured headers, including ETag by default.
  • Bug Fixes
    • Prevented invalid CORS configurations from causing startup failures.
    • Improved deletion retry behavior and preserved cancellation metadata.
  • Documentation
    • Clarified supported OData filtering and sorting behavior.

diffora added 6 commits July 17, 2026 13:32
The RBAC/AM write contract is ETag-guarded: DELETE/PATCH on role
assignments and role definitions require the resource's current ETag in
If-Match, and the only way to obtain it is to read the ETag header from a
GET/POST response. Browsers may only read non-safelisted response headers
when the server sends Access-Control-Expose-Headers, and the gateway sent
none at all - so every cross-origin browser client saw ETag as absent and
every ETag-guarded write dead-ended, while curl/Postman hid the problem.

Add CorsConfig.exposed_headers (wired to CorsLayer::expose_headers,
"*" supported like the sibling lists) defaulting to ["ETag"] so
deployments get the fix without a config change, and cover both the
default and an explicitly configured list with request-level tests.

Signed-off-by: Diffora <ddiffora@gmail.com>
…ren listing

Clicking the Status column on the Tenants page issued
GET /tenants/{id}/children?$orderby=status and got 400
'unsupported $orderby field: status': TenantODataMapper::is_orderable
rejected the field because the cursor codec was hard-wired to the wire
filter kind (String) while the extracted cursor value spoke the storage
shape (SMALLINT) - encode/parse could not round-trip the token.

Give ODataFieldMapping a per-field cursor_kind() (default: the wire
field.kind(), so every existing mapper is untouched) and use it on both
sides of the cursor codec. The AM mapper overrides Status -> I64 and
extracts the ordinal as BigInt, so status becomes orderable by its
lifecycle ordinal (active < suspended < deleted) with cursor pages
comparing integer-to-SMALLINT - a numeric SQL comparison.

tenant_type stays non-orderable (ordering by a derived UUIDv5 has no
honest meaning). Filter comparisons on status stay membership-only
(eq/ne/in). Integration tests replace the old rejection pin with a
lifecycle-ordinal sort assertion and a full cursor walk across ordinal
groups.

Signed-off-by: Diffora <ddiffora@gmail.com>
… rejections

POST /tenants/{id}/users with a duplicate username came back as the
redacted generic 400 invalid_argument (field=request, reason=VALIDATION,
'identity provider rejected the user operation (detail redacted; ...)').
The client could not attribute the failure to the username field, which
blocks the PRD 'Add user dialogs - per-field and API error handling'
acceptance criterion. Duplicate email and password-policy rejections
collapsed into the same catch-all.

SDK: extend the #[non_exhaustive] IdpUserOperationFailure with typed
variants - DuplicateUser { field: IdpUserDuplicateField (Username |
Email), detail } and PasswordPolicy { detail }; Rejected stays as the
fallback for unattributable rejections, so legacy plugins keep today's
behavior unchanged. New field/reason vocabulary: PASSWORD_FIELD,
PASSWORD_POLICY.

AM: map DuplicateUser to the new DomainError::UserAlreadyExists ->
HTTP 409 already_exists on the user resource type, with the stable
colliding-field token ('username' / 'email') as resource_name (never
the caller-supplied value); map PasswordPolicy to
DomainError::IdpPasswordPolicy -> HTTP 400 with field_violations
[{field: password, reason: PASSWORD_POLICY}] instead of the generic
request/VALIDATION pair. Raw provider text stays digest-only in the
am.idp log lines, same redaction posture as the existing arms.

The vp-idp-plugin change that actually emits the classified variants
from Keycloak 409/400 responses lands separately in vhp-core after the
submodule bump - until then every path still produces Rejected and
nothing changes on the wire.

Signed-off-by: Diffora <ddiffora@gmail.com>
…-delete TX

DELETE /tenants/{id} on a tenant with an open child-conversion request
returned 204 and tombstoned the tenant, but the conversion row stayed
'pending' with tenant_id pointing at the tombstone - breaking the
invariant that every pending conversion references a live tenant, and
leaving LIST /child-conversions dereferencing a dead FK.

schedule_deletion's SERIALIZABLE TX now terminal-resolves the request
right after the status flip + closure rewrite: a guarded UPDATE
(status = Pending AND deleted_at IS NULL) moves it to Cancelled with
cancelled_by = the deleting actor, resolved_at = the deletion
timestamp, and a fixed 'subject tenant deleted' comment. The helper
lives with the conversion repo (cancel_pending_on_tenant_delete_tx) so
ownership of the conversion_requests transition shape stays in one
place; the tenant TX just invokes it. The Pending fence keeps
idempotent delete retries from re-stamping an already-resolved row,
and the already-Deleted short-circuit returns before the cascade
anyway. Only rows keyed by the deleted tenant can exist here: a parent
with non-deleted children is rejected by TenantHasChildren before the
TX.

schedule_deletion gains a deleted_by actor parameter (trait, impl,
in-memory fake, all call sites); the service passes ctx.subject_id().
An am.events line (conversion_auto_cancelled_on_tenant_delete) fires
when the cascade touches a row.

Integration tests: cascade resolves the row in the same TX (row is
resolved, not removed; no pending row survives for the tombstone), an
idempotent retry with a different actor/timestamp preserves the first
resolution verbatim, and a sibling tenant's pending conversion is
untouched.

Signed-off-by: Diffora <ddiffora@gmail.com>
…ting/conversion fixes

Fixes from the branch code review, batched:

api-gateway:
- validate_cors_config at gear init rejects any wildcard CORS list
  combined with allow_credentials=true. tower-http asserts on exactly
  this inside Layer::layer, and axum's eager router layering turned it
  into a startup crash-loop with no pointer at the config; now it is a
  clean init error naming the offending list.
- Unparseable entries in ANY cors list warn and are skipped instead of
  vanishing in a silent filter_map: a typo'd exposed_headers entry used
  to silently suppress Access-Control-Expose-Headers and re-break every
  ETag-guarded browser write. Shared parse_list helper replaces the 4th
  copy of the parse pattern.

account-management SDK+AM:
- IdpUserDuplicateField gains UsernameOrEmail: Keycloak's
  ModelDuplicateException path emits the combined constant 'User exists
  with same username or email' (on KC 26 it is the only 409 createUser
  produces directly), which must not be misattributed to username.
- DomainError::UserAlreadyExists now carries the typed field; the
  canonical boundary derives both resource_name and the curated detail
  from it, collapsing three copies of the wording into one.

account-management:
- The conversion auto-cancel am.events line is emitted AFTER the
  SERIALIZABLE TX commits (count rides the closure return): logging
  inside the closure double-counted on a 40001 retry and emitted a
  phantom event when the TX ultimately rolled back.
- The already-Deleted idempotent short-circuit now also runs the
  (Pending-fenced) cancel cascade, so tenants soft-deleted BEFORE the
  cascade existed are healed by the natural operator remediation -
  retrying the DELETE; post-fix tombstones make it a no-op.
- OpenAPI account-management-v1.yaml: $orderby=status on the tenant
  children listing is now documented as supported with lifecycle-ordinal
  semantics (the old text still declared it rejected with 400).

toolkit-db + resource-group (follow-through):
- encode_cursor_value accepts SmallInt/Int under FieldKind::I64, so
  mappers exposing narrow integer columns need not hand-widen to BigInt
  (AM's status extract reverts to the natural SmallInt).
- resource-group's three type fields (Group/Hierarchy/Membership) had
  the exact wire-String/storage-SmallInt mismatch the cursor_kind seam
  governs, orderable and un-overridden - $orderby=type broke with
  InvalidCursor on any page overflow; they now declare cursor_kind=I64.
- Codec unit tests pin the narrow-integer round-trip and the loud
  failure for a genuine wire/storage mismatch.

Signed-off-by: Diffora <ddiffora@gmail.com>
…m actor

The retention hard-delete reaper could never purge a tenant: its RG
cascade hook ran under a system-actor context scoped to the very
tenant being hard-deleted. The authz resolver's system-actor grant
roots a tenant-subtree scope at the subject's home tenant - a Deleted
root is clamped out of scope materialization, the subtree resolves to
zero tenants, and every cascade call fails closed, so each due tenant
was deferred forever (observed on stage1: processed=64 cleaned=0
deferred=64 on every tick).

Make for_user_groups_cascade platform-scoped (nil tenant -> Global
grant at the resolver): honest for this flow, which addresses explicit
group ids of a tenant being erased, not a live tenant's data. The
deleted tenant id stays on the am.system_actor audit line. The
resolver-side halves (empty-token-scopes bypass for the unforgeable
system actor + Global on nil home tenant) land in vp-core's
vp-authz-resolver-plugin.

Signed-off-by: Diffora <ddiffora@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@diffora, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6bbf00cf-1ef2-45fe-8603-5388bd03decd

📥 Commits

Reviewing files that changed from the base of the PR and between 8cb09bf and 222d78d.

📒 Files selected for processing (4)
  • gears/system/resource-group/resource-group-sdk/src/error_tests.rs
  • gears/system/resource-group/resource-group-sdk/src/gts.rs
  • gears/system/resource-group/resource-group/src/api/rest/error.rs
  • gears/system/resource-group/resource-group/tests/domain_unit_test.rs
📝 Walkthrough

Walkthrough

The PR adds structured IdP failure types and canonical mappings, cascades tenant deletion to pending conversions, supports storage-shaped OData cursors for status ordering, and adds configurable CORS response-header exposure with startup validation.

Changes

Structured IdP error handling

Layer / File(s) Summary
IdP failure and field contracts
gears/system/account-management/account-management-sdk/src/{field.rs,idp_user.rs,lib.rs}
The SDK adds duplicate-user and password-policy failure variants, duplicate-field helpers, and canonical password field and reason tokens.
Domain error conversion
gears/system/account-management/account-management/src/domain/{error.rs,error_tests.rs}, .../domain/idp/*
Structured IdP failures map to typed domain errors with redacted provider details and tested status/code mappings.
Canonical API error mapping
gears/system/account-management/account-management/src/infra/{sdk_error_mapping.rs,sdk_error_mapping_tests.rs}
Domain errors map to user-resource already_exists and password field-violation envelopes.

Tenant deletion cascades

Layer / File(s) Summary
Deletion security context
gears/system/account-management/account-management/src/domain/system_actor.rs
The user-groups cascade context uses a nil tenant and has dedicated coverage.
Deletion actor propagation
gears/system/account-management/account-management/src/domain/tenant/*, .../infra/storage/repo_impl/mod.rs, .../tests/*
schedule_deletion accepts and forwards deleted_by, with callers, fakes, and existing tests updated.
Transactional conversion cancellation
gears/system/account-management/account-management/src/infra/storage/repo_impl/{conversion.rs,updates.rs}, .../tests/conversion_integration.rs
Pending conversions are cancelled during tenant deletion, retries remain idempotent, cancellation events are emitted, and sibling tenants are unaffected.

Storage-shaped OData cursors

Layer / File(s) Summary
Cursor kind contract
libs/toolkit-db/src/odata/sea_orm_filter.rs
Cursor mappings can override wire field kinds, and integer cursor encoding accepts narrow database integer values.
Resource mapper cursor kinds
gears/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs, gears/system/resource-group/resource-group/src/infra/storage/odata_mapper.rs
Resource mappers select integer cursor kinds for storage-backed categorical fields.
Tenant status ordering and API contract
gears/system/account-management/account-management/tests/list_children_integration.rs, .../account-management-sdk/src/tenant.rs, .../docs/account-management-v1.yaml
Tenant status ordering and cursor pagination are enabled and documented using lifecycle ordinal ordering.

CORS header exposure

Layer / File(s) Summary
CORS exposure configuration
gears/system/api-gateway/src/{config.rs,cors.rs}
CORS configuration adds exposed headers, validates wildcard credential combinations, and warns on invalid list entries.
Startup validation
gears/system/api-gateway/src/gear.rs
Gateway initialization rejects invalid CORS configuration before router construction.
Exposed response headers
gears/system/api-gateway/tests/cors_tests.rs
Integration tests verify default and explicit response-header exposure.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IdP
  participant UserOperationFailureExt
  participant sdk_error_mapping
  IdP->>UserOperationFailureExt: DuplicateUser or PasswordPolicy
  UserOperationFailureExt->>sdk_error_mapping: DomainError
  sdk_error_mapping-->>IdP: CanonicalError envelope
Loading

Suggested reviewers: artifizer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the batch release-bug focus across CORS, AM/IdP errors, conversions, and system-actor fixes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch VHP-2193

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Fold the reasoning previously carried by internal tracker references
directly into the comments so they stand on their own.

Signed-off-by: Diffora <ddiffora@gmail.com>
@diffora diffora changed the title VHP-2193/2084/2158/1729/2376: 26.7 release-bug batch (api-gateway CORS, AM listing/IdP-errors/conversions, authz system-actor) 26.7 release-bug batch: api-gateway CORS, AM listing / IdP errors / conversions, authz system-actor Jul 17, 2026
@diffora
diffora marked this pull request as ready for review July 17, 2026 16:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@gears/system/account-management/account-management/tests/list_children_integration.rs`:
- Around line 511-533: The cursor pagination loop in the integration test must
be bounded so repeated next_cursor values cannot hang indefinitely. Update the
loop around cursor and page.page_info.next_cursor to track previously seen
cursors or enforce a maximum page count, and fail with a clear assertion when
the bound is exceeded while preserving normal pagination and collection
behavior.

In `@gears/system/account-management/docs/account-management-v1.yaml`:
- Around line 761-764: Update the wire/storage divergence description in the
account-management API documentation to state that filters and response bodies
both use public enum strings, while the divergence is between the wire contract
and SMALLINT database storage. Remove the claim that filters differ from the
response shape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bbd823b2-67f5-4796-b67c-025364617de0

📥 Commits

Reviewing files that changed from the base of the PR and between e7361e7 and 8cb09bf.

📒 Files selected for processing (32)
  • gears/system/account-management/account-management-sdk/src/field.rs
  • gears/system/account-management/account-management-sdk/src/idp_user.rs
  • gears/system/account-management/account-management-sdk/src/lib.rs
  • gears/system/account-management/account-management-sdk/src/tenant.rs
  • gears/system/account-management/account-management/src/domain/error.rs
  • gears/system/account-management/account-management/src/domain/error_tests.rs
  • gears/system/account-management/account-management/src/domain/idp/idp_tests.rs
  • gears/system/account-management/account-management/src/domain/idp/mod.rs
  • gears/system/account-management/account-management/src/domain/system_actor.rs
  • gears/system/account-management/account-management/src/domain/tenant/repo.rs
  • gears/system/account-management/account-management/src/domain/tenant/service/mod.rs
  • gears/system/account-management/account-management/src/domain/tenant/service/service_tests.rs
  • gears/system/account-management/account-management/src/domain/tenant/test_support/repo.rs
  • gears/system/account-management/account-management/src/infra/sdk_error_mapping.rs
  • gears/system/account-management/account-management/src/infra/sdk_error_mapping_tests.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/mod.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/updates.rs
  • gears/system/account-management/account-management/tests/conversion_integration.rs
  • gears/system/account-management/account-management/tests/lifecycle_integration.rs
  • gears/system/account-management/account-management/tests/list_children_integration.rs
  • gears/system/account-management/account-management/tests/metadata_integration.rs
  • gears/system/account-management/account-management/tests/metadata_integration_pg.rs
  • gears/system/account-management/account-management/tests/updates_integration.rs
  • gears/system/account-management/docs/account-management-v1.yaml
  • gears/system/api-gateway/src/config.rs
  • gears/system/api-gateway/src/cors.rs
  • gears/system/api-gateway/src/gear.rs
  • gears/system/api-gateway/tests/cors_tests.rs
  • gears/system/resource-group/resource-group/src/infra/storage/odata_mapper.rs
  • libs/toolkit-db/src/odata/sea_orm_filter.rs

Comment on lines +511 to +533
let mut collected = Vec::new();
let mut cursor: Option<String> = None;
loop {
let mut q = ODataQuery::default().with_limit(2);
if let Some(c) = cursor.take() {
q = q.with_cursor(CursorV1::decode(&c).expect("decode status cursor"));
} else {
q = q.with_order(ODataOrderBy(vec![OrderKey {
field: "status".to_owned(),
dir: SortDir::Asc,
}]));
}
let page = h
.repo
.list_children(&allow_all(), root, &q)
.await
.expect("status-ordered cursor page must not fail");
collected.extend(ids_of(&page.items));
match page.page_info.next_cursor {
Some(next) => cursor = Some(next),
None => break,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Bound the cursor walk to prevent a hanging regression test.

A repeated next_cursor causes this loop to run indefinitely instead of failing with a useful assertion. Track seen cursors or cap the number of pages.

Proposed fix
+    let mut seen_cursors = std::collections::HashSet::new();
     let mut collected = Vec::new();
     let mut cursor: Option<String> = None;
     loop {
         let mut q = ODataQuery::default().with_limit(2);
         if let Some(c) = cursor.take() {
+            assert!(seen_cursors.insert(c.clone()), "pagination returned a repeated cursor");
             q = q.with_cursor(CursorV1::decode(&c).expect("decode status cursor"));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let mut collected = Vec::new();
let mut cursor: Option<String> = None;
loop {
let mut q = ODataQuery::default().with_limit(2);
if let Some(c) = cursor.take() {
q = q.with_cursor(CursorV1::decode(&c).expect("decode status cursor"));
} else {
q = q.with_order(ODataOrderBy(vec![OrderKey {
field: "status".to_owned(),
dir: SortDir::Asc,
}]));
}
let page = h
.repo
.list_children(&allow_all(), root, &q)
.await
.expect("status-ordered cursor page must not fail");
collected.extend(ids_of(&page.items));
match page.page_info.next_cursor {
Some(next) => cursor = Some(next),
None => break,
}
}
let mut seen_cursors = std::collections::HashSet::new();
let mut collected = Vec::new();
let mut cursor: Option<String> = None;
loop {
let mut q = ODataQuery::default().with_limit(2);
if let Some(c) = cursor.take() {
assert!(seen_cursors.insert(c.clone()), "pagination returned a repeated cursor");
q = q.with_cursor(CursorV1::decode(&c).expect("decode status cursor"));
} else {
q = q.with_order(ODataOrderBy(vec![OrderKey {
field: "status".to_owned(),
dir: SortDir::Asc,
}]));
}
let page = h
.repo
.list_children(&allow_all(), root, &q)
.await
.expect("status-ordered cursor page must not fail");
collected.extend(ids_of(&page.items));
match page.page_info.next_cursor {
Some(next) => cursor = Some(next),
None => break,
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@gears/system/account-management/account-management/tests/list_children_integration.rs`
around lines 511 - 533, The cursor pagination loop in the integration test must
be bounded so repeated next_cursor values cannot hang indefinitely. Update the
loop around cursor and page.page_info.next_cursor to track previously seen
cursors or enforce a maximum page count, and fail with a clear assertion when
the bound is exceeded while preserving normal pagination and collection
behavior.

Comment on lines +761 to +764
it. Response bodies return the same field as a string
(`"pending"`, `"active"`, etc.) for human readability — the
filter is the one place the wire surface diverges from the
response shape.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the wire/storage divergence description.

Filters and responses both use public enum strings; the divergence is between the wire contract and SMALLINT storage, not between filters and responses.

Proposed fix
-        it. Response bodies return the same field as a string
-        (`"pending"`, `"active"`, etc.) for human readability — the
-        filter is the one place the wire surface diverges from the
-        response shape.
+        it. Filters and response bodies use the same public string
+        values (`"pending"`, `"active"`, etc.); only the internal
+        storage representation uses numeric ordinals.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it. Response bodies return the same field as a string
(`"pending"`, `"active"`, etc.) for human readability — the
filter is the one place the wire surface diverges from the
response shape.
it. Filters and response bodies use the same public string
values (`"pending"`, `"active"`, etc.); only the internal
storage representation uses numeric ordinals.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gears/system/account-management/docs/account-management-v1.yaml` around lines
761 - 764, Update the wire/storage divergence description in the
account-management API documentation to state that filters and response bodies
both use public enum strings, while the divergence is between the wire contract
and SMALLINT database storage. Remove the claim that filters differ from the
response shape.

… evaluation

The gts-rust v0.11.0 migration mechanically replaced the group gate's
resource-type literal with the crate-name-derived SDK constant, which
silently renamed the PEP-evaluated type from gts.cf.core.rg.group.v1~
to gts.cf.core.resource_group.group.v1~. Every deployed role grant is
written against the documented gts.cf.core.rg.* family, so the rename
403'd all group CRUD for tenant members and failed AM's tenant-delete
ownership probe closed (surfaced as 503) - verified live: a grant on
gts.cf.core.rg.* is denied while gts.cf.core.resource_group.* is
allowed, on the same caller and flow.

Pin GROUP_RESOURCE_TYPE back to cf.core.rg.group.v1~, consistent with
the untouched siblings (rg.group_membership.v1~, rg.type.v1~), and
realign the #[resource_error] tags + tests that the SDK round-trip
tests pin to the constant.

Signed-off-by: Diffora <ddiffora@gmail.com>
@diffora

diffora commented Jul 17, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #10 (clean history + branch name, CodeRabbit's two comments folded in). Closing this one; the VHP-2193 branch is being removed.

@diffora diffora closed this Jul 17, 2026
@diffora
diffora deleted the VHP-2193 branch July 17, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant