Skip to content

Review: Full prototype implementation#49

Draft
Schmarvinius wants to merge 47 commits into
review-basefrom
main
Draft

Review: Full prototype implementation#49
Schmarvinius wants to merge 47 commits into
review-basefrom
main

Conversation

@Schmarvinius

Copy link
Copy Markdown
Contributor

This PR contains the full prototype implementation for review purposes.

Note: This PR is for code review only — do not merge it. Close it once the review is complete.

lisajulia and others added 30 commits May 6, 2026 16:06
…tup.java

Co-authored-by: Matthias Braun <matthia.braun@sap.com>
Co-authored-by: Simon Engel <simon.engel01@sap.com>
Co-authored-by: Simon Engel <simon.engel01@sap.com>
Replaces the single-module srv/ codebase with the multi-module Maven
layout developed on the internal SAP fork. Adds LICENSE at root (per OSPO
finding), pom.xml SCM section, and fixes the broken .reuse/dep5 link in
CONTRIBUTING.md.

The .github/ workflows and gh_ruleset.json from this repo are preserved.

Note: integration-tests/spring tests fail on clean clone because the
build does not invoke npm install before cds build, so the @cap-js/ai
node plugin is not available when the csn is generated. Pre-existing
issue on the fork's main; tracked separately.
Co-authored-by: hyperspace-insights[bot] <209611008+hyperspace-insights[bot]@users.noreply.github.com>
* chore: bootstrap ci/cd workflows

* chore: address pr bot review findings

* fix: specify ai-core service instance and key

* rename blackduck identifier
* fix: run npm ci before cds build in integration-tests (#17)

Without `npm ci`, `cds build` ran in `integration-tests/spring/` without the
`@cap-js/ai` Node plugin, so the generated CSN was missing the synthetic
`SAP_Recommendations` association and 8 RecommendationTest cases failed on
clean clone with `JsonPath $['SAP_Recommendations']: PathNotFoundException`.

Add `cds.install-node` and `cds.npm-ci` executions to the cds-maven-plugin
block in `integration-tests/spring/pom.xml`, mirroring the pattern already
used in `samples/bookshop/srv/pom.xml`. CI continues to skip the Node
download via `-Dcds.install-node.skip`; clean local clones get a
self-contained Node + `npm ci` into `integration-tests/node_modules/`.

Commit `package-lock.json` for `integration-tests/` and `samples/bookshop/`
so `npm ci` is reproducible, and remove the `**/package-lock.json` entry
from `.gitignore`.

Closes #17

* fix: regenerate lockfiles against public npm registry

The @cap-js/ai entry in the previous lockfiles pointed at
int.repositories.cloud.sap (SAP-internal Artifactory), which is unreachable
from GitHub-hosted runners and broke `npm ci` in CI with EAI_AGAIN.

Regenerated both lockfiles with the public registry forced for the @cap-js
scope so they resolve from registry.npmjs.org.

* fix: invert integration-test modules profile and suppress AICore kinds models

Two fixes wrapped together since they both unblock CI on PR #21.

1. Maven `<modules>` is additive in profiles, so `skip-integration-tests`
   was a no-op — it re-listed the source modules instead of removing the
   integration ones. Invert the structure: default modules are source-
   only; a `with-integration-tests` profile (active by default) adds
   `integration-tests` and `coverage-report`. CI consumers switch from
   `-P skip-integration-tests` to `-P '!with-integration-tests'`.

2. In CI, `cds bind --exec` writes a `[hybrid]` profile entry to
   `.cdsrc-private.json` for the `ai-core` binding, kind `AICore-btp`.
   `cds build` then expands the kind's `model: "@cap-js/ai/srv/AICoreService"`
   into the build's model list, which collides with the `AICore` service
   shipped by `cds-feature-ai-core` (loaded via `using { AICore } from
   'com.sap.cds/ai'`). The existing `requires.AICore.model = false`
   override only suppresses the named-require model, not the kind's.
   Add `requires.kinds.AICore-btp.model = false` (and the same for
   `AICore-mocked`) so the kind never contributes a model to cds build.

* fix: build root modules before sonar scan; dispatch event handlers on registered service type

- SonarQube scan job ran `mvn clean verify` from `integration-tests/`
  without first installing the source modules, so the integration-tests
  build couldn't resolve `cds-feature-ai-core` etc. as JAR dependencies.
  Mirror the integration-tests action: install the three source modules
  with `-pl ... -am -DskipTests` first, then run the scan build.

- `AICoreServiceConfiguration.eventHandlers` recomputed `hasAICoreBinding`
  separately from `services()` and unconditionally cast the registered
  service to `AICoreServiceImpl`. If the two calls disagreed (or if
  another configurer registered a Mock first), the cast threw
  `ClassCastException: MockAICoreServiceImpl cannot be cast to
  AICoreServiceImpl`. Read the actually-registered service from the
  catalog and dispatch via `instanceof` patterns so the handler set
  always matches the live registration.
The Fiori recommendation handler used to emit a SAP_Recommendations entry
for every column in the prediction response, regardless of whether the
user had already filled that column. This worked locally because the
mock client only returns predictions for cells marked [PREDICT], but it
broke in CI where the real RPT model returns predictions for every
target column. Filter the response to columns that were null in the
input row so behaviour is consistent across both clients.

Adds a regression unit test that simulates the RPT-style response shape.
#25)

* test(itest): disable ResourceGroup mutating tests pending admin scopes (#24)

The 4 CREATE/DELETE tests in ResourceGroupTest fail in CI with HTTP 403
because /v2/admin/resourceGroups requires AI Core tenant-admin scopes
(see SAP AI Core service guide §3.2 / §6.3), which the bound CI service
key (sap-internal plan, technical user cap.calesi.munich@sap.com) does
not carry. Disable them with @disabled until the IAM situation is sorted;
read-only readAll_returnsResourceGroups stays enabled.

Refs #24

* fix(itest): repair resource-group cleanup so quota does not leak (#24)

The test extension's pre-cleanup never ran in CI because it gated on
AICORE_SERVICE_KEY, but the bound credentials are exposed via VCAP_SERVICES.
48 itest-rg-* groups had leaked into the cdsmunich tenant, hitting the
50-group hard quota and turning every CREATE into a 400/quota error.

- Remove the env-var guard from ResourceGroupCleanupExtension so the
  ResourceGroupApi auto-discovers credentials from VCAP_SERVICES (same
  pattern as production AICoreServiceImpl).
- Add cleanup-resource-groups.cjs and invoke it with if: always() in the
  integration-tests action, mirroring cap-js/ai. Survives JVM crashes.
- Re-enable the four ResourceGroupTest CREATE/DELETE tests.

* chore(itest): drop unused .cjs cleanup script

The JUnit ResourceGroupCleanupExtension already handles cleanup
exhaustively in @BeforeAll/@afterall — verified green in the previous
CI run. The .cjs script failed with 'No impl configured for
cds.requires.kinds.AICore-btp' anyway and was wrapped in `|| true`,
so it provided no real safety net.
Bumps the minor-patch group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [com.github.ben-manes.caffeine:caffeine](https://github.com/ben-manes/caffeine) | `3.2.0` | `3.2.4` |
| [org.apache.maven.plugins:maven-enforcer-plugin](https://github.com/apache/maven-enforcer) | `3.6.2` | `3.6.3` |
| [com.diffplug.spotless:spotless-maven-plugin](https://github.com/diffplug/spotless) | `3.4.0` | `3.5.0` |
| [com.sap.ai.sdk:core](https://github.com/SAP/ai-sdk-java) | `1.18.0` | `1.19.0` |
| [com.sap.ai.sdk.foundationmodels:sap-rpt](https://github.com/SAP/ai-sdk-java) | `1.18.0` | `1.19.0` |



Updates `com.github.ben-manes.caffeine:caffeine` from 3.2.0 to 3.2.4
- [Release notes](https://github.com/ben-manes/caffeine/releases)
- [Commits](ben-manes/caffeine@v3.2.0...v3.2.4)

Updates `org.apache.maven.plugins:maven-enforcer-plugin` from 3.6.2 to 3.6.3
- [Release notes](https://github.com/apache/maven-enforcer/releases)
- [Commits](apache/maven-enforcer@enforcer-3.6.2...enforcer-3.6.3)

Updates `com.diffplug.spotless:spotless-maven-plugin` from 3.4.0 to 3.5.0
- [Release notes](https://github.com/diffplug/spotless/releases)
- [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md)
- [Commits](diffplug/spotless@maven/3.4.0...maven/3.5.0)

Updates `com.sap.ai.sdk:core` from 1.18.0 to 1.19.0
- [Release notes](https://github.com/SAP/ai-sdk-java/releases)
- [Changelog](https://github.com/SAP/ai-sdk-java/blob/main/docs/release_notes.md)
- [Commits](SAP/ai-sdk-java@rel/1.18.0...rel/1.19.0)

Updates `com.sap.ai.sdk.foundationmodels:sap-rpt` from 1.18.0 to 1.19.0
- [Release notes](https://github.com/SAP/ai-sdk-java/releases)
- [Changelog](https://github.com/SAP/ai-sdk-java/blob/main/docs/release_notes.md)
- [Commits](SAP/ai-sdk-java@rel/1.18.0...rel/1.19.0)

Updates `com.sap.ai.sdk.foundationmodels:sap-rpt` from 1.18.0 to 1.19.0
- [Release notes](https://github.com/SAP/ai-sdk-java/releases)
- [Changelog](https://github.com/SAP/ai-sdk-java/blob/main/docs/release_notes.md)
- [Commits](SAP/ai-sdk-java@rel/1.18.0...rel/1.19.0)

---
updated-dependencies:
- dependency-name: com.github.ben-manes.caffeine:caffeine
  dependency-version: 3.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: org.apache.maven.plugins:maven-enforcer-plugin
  dependency-version: 3.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: com.diffplug.spotless:spotless-maven-plugin
  dependency-version: 3.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: com.sap.ai.sdk:core
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: com.sap.ai.sdk.foundationmodels:sap-rpt
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: com.sap.ai.sdk.foundationmodels:sap-rpt
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

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

* fix(mtx): wire MTX subscribe lifecycle for mock AI Core service (#26)

The local-with-tenants profile didn't set cds.requires.AICore.multiTenancy,
so AICoreServiceImpl.isMultiTenancyEnabled() returned false and seven MTX
integration tests failed. Even with the flag, the mock service had no
SubscribeEventContext handler, so the tenant resource-group cache was never
populated and the cache-based assertions still failed.

- Enable cds.requires.AICore.multiTenancy under the local-with-tenants
  profile so MT registers in mtx-local integration tests.
- Add MockAICoreSetupHandler mirroring AICoreSetupHandler's subscribe/
  unsubscribe lifecycle in-memory (no AI Core API calls).
- Register the mock setup handler from AICoreServiceConfiguration when
  the mock service runs with MT enabled.
- Use AICoreService interface in mtx-local integration tests; drop the
  AICoreServiceImpl casts and the assumeTrue skips that those casts forced.

* ci: split mtx-local into its own job without AI Core binding (#26)

Running spring + mtx-local under one cds-bind made mtx-local inherit the
AI Core binding, so AICoreServiceConfiguration registered the real
AICoreServiceImpl and AICoreSetupHandler hit the live API on every
subscribe/unsubscribe (HTTP 409 on resource-group delete in CI). The
mtx-local module is intended to run in-memory against the mock service.

- Add a Local MTX Tests job (matrix Java 17/21, no cf-bind) that runs
  mvn -pl integration-tests/mtx-local/srv -am -P mtx-integration-tests.
- Drop the mtx-integration-tests profile from the integration-tests
  composite action so its cds-bind invocation runs only the spring
  reactor (the default).

* change default rg and add provisioning to itest

* fix(itest): avoid 404 on keyed RG read by listing then filtering

A keyed READ on AICore.resourceGroups routes through
ResourceGroupHandler.onRead -> resourceGroupApi.get(rgId), which
returns 404 for a not-yet-existing RG instead of an empty list.
Use a non-keyed Select and filter client-side so the helper can
detect absence and INSERT the RG.

* fix(itest): increase RPT deployment poll budget to ~6 min

Prior 10-attempt cap (~145 s sleep budget) was right at the edge of the
observed RPT-1 deployment time (~150 s) and timed out in CI. Bumping to
18 attempts gives a ~6 min ceiling so cold deployments have headroom
without making genuinely failed runs hang indefinitely.

* fix(itest): tune RPT poll budget to 15 attempts (~4.7 min)

18 attempts gave too much headroom; 15 is enough margin over the
observed ~150 s RPT-1 deployment time without making genuinely stuck
runs hang for too long.

* fix(itest): scope cleanup to label-owned RGs to protect other CF instances

AI Core resource groups are shared across all CF service instances bound
to the same AI Core tenant, so listing every RG and stopping its
deployments before delete was destroying work owned by other instances.

Adopt cap-js/ai's pattern (srv/ai-core/resourceGroups.js) of stamping
each RG with an owner label at create time and filtering by it on
cleanup, but use a distinct key (CDS_FEATURE_AI_ITEST_OWNER) so the
itest cleanup never collides with prod tenant cleanup. Cleanup also
short-circuits when the owner is unset or equal to the local-dev RG
name, so dev runs reuse their RG across invocations.

While here, drop the test scaffold's hand-rolled config/deployment
helpers and call the existing AICoreServiceImpl.deploymentId(...) which
already does lookup-or-create-with-polling. The RG insert + provisioned
poll stay in test scope per design.

* fix(itest): drop redundant null-check on rg.getLabels()

BckndResourceGroup.getLabels() is @nonnull, so the null guard was
flagged by SpotBugs (RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE).

* fix(ci): resolve integration test and SonarQube pipeline failures

- ResourceGroupCleanupExtension: defer afterAll cleanup to end of test
  suite using CloseableResource pattern, preventing premature deletion
  of the resource group before all test classes have completed
- TenantIsolationTest: catch Throwable (not just Exception) in tearDown
  so MockMvc AssertionError does not escape the safety catch blocks
- SonarQube scan: remove -P mtx-integration-tests from the verify step;
  mtx-local tests already run in their dedicated local-mtx-tests job
  and including them under cds bind causes real AI Core API calls that
  conflict with the mock-based test design
- BaseIntegrationTest: expose clearDeploymentIdCache() so the cleanup
  extension can invalidate stale cached deployment IDs after deletion

* fix(itest): prevent beforeAll cleanup from deleting active resource group

The ResourceGroupCleanupExtension.beforeAll() was deleting the resource
group that AICoreServiceTest (which runs first) had just created, causing
ActionTest and all Recommendation tests to fail with 400/500 from AI Core.

- Remove beforeAll/cleanupOnce from ResourceGroupCleanupExtension; the
  deferred afterAll (CloseableResource at suite end) is sufficient
- Expose ensureResourceGroupProvisioned() as protected in BaseIntegrationTest
- Add @BeforeAll to ActionTest to guarantee the resource group is
  provisioned before deploymentId tests run, regardless of class order

* fix(ci): generate JaCoCo XML reports for SonarQube scan

- Add jacoco report goal to cds-feature-ai-core and integration-tests/spring
  pom.xml (they only had prepare-agent, no report generation)
- Move mtx-local dependency in coverage-report to mtx-integration-tests
  profile so the aggregate report can be built without mtx-local
- Update scan-with-sonar action: remove -DskipTests so unit tests run and
  generate coverage; add install to integration tests step; add step to
  build coverage-report aggregate; point sonar at the aggregate report

* fix(itest): prevent ResourceGroupTest from leaking resource groups

ResourceGroupTest creates itest-rg-* groups during tests but AfterEach
cleanup silently failed (409 Conflict) because the RG wasn't provisioned
yet. Additionally, the ResourceGroupCleanupExtension only cleaned
label-owned groups, missing the itest-rg-* prefix groups entirely.

- ResourceGroupTest: wait for RG to be provisioned before delete in
  AfterEach, matching the pattern used in delete_resourceGroup test
- ResourceGroupCleanupExtension: add prefix-based safety-net cleanup
  that always deletes any itest-rg-* groups at suite end, regardless
  of whether CDS_AICORE_TEST_RESOURCE_GROUP is set
- scan-with-sonar: set CDS_AICORE_TEST_RESOURCE_GROUP env var so the
  cleanup extension actually runs in the SonarQube scan job

* fix(itest): stop_deployment test must use own RG, not 'default'

The test was hardcoded to query and stop the first RUNNING deployment
in the 'default' resource group. Since AI Core is scoped at the
subaccount level, this stopped deployments from other CF spaces sharing
the same subaccount (e.g. orchestration deployments).

Use getDefaultResourceGroup() which resolves to the CI-specific
itest-* resource group, ensuring only test-owned deployments are
affected.

* fix(itest): remove all hardcoded 'default' resource group references

DeploymentTest.update_targetStatus_stopsRunningDeployment was still
hardcoded to query and stop deployments in the 'default' resource group,
which stopped the user's orchestration deployment in the shared
subaccount. Also fix ConfigurationTest to avoid creating orphan configs
in 'default'.

All test classes now use getDefaultResourceGroup() which resolves to the
CI-specific itest-* resource group via CDS_AICORE_TEST_RESOURCE_GROUP.

* fix: retry on HTTP 404 from AI Core inference endpoint

AI Core deployments report RUNNING status before the inference gateway
has fully propagated the route. This causes 404 responses for a short
window after deployment creation. The retry logic already handled 403
and 412 as transient 'not ready yet' states but was missing 404.

Adding 404 to the retryable status codes makes inference calls resilient
to this AI Core eventual-consistency window.

* fix: cap exponential backoff at 30s and increase itest retry budget

The exponential backoff with no cap resulted in extremely long final
intervals (77s, 154s) leaving only ~153s of total retry window across
10 attempts. For fresh deployments where inference needs up to 5 min
to become stable, this was insufficient.

- Cap max interval at 30s so retries stay frequent
- Increase integration test maxRetries to 15 (gives ~5.5 min window)
- Together with the 404 retry fix, this handles AI Core's deployment
  warm-up period reliably

* fix(ci): move RG cleanup to dedicated job after all itests complete

When Java 17 and Java 21 integration tests run in parallel, they create
separate deployments of the same RPT model. When the first-finishing job
cleaned up (stopped its deployment + deleted its RG), AI Core tore down
shared model serving infrastructure causing the still-running job's
inference calls to fail with 404.

- ResourceGroupCleanupExtension: afterAll now only cleans itest-rg-*
  leaked groups (safety net), no longer stops/deletes the main test RG
- pipeline.yml: add dedicated 'integration-tests-cleanup' job that runs
  with if:always() after BOTH integration test jobs complete, then
  deletes all resource groups for the current run

* fix(ci): remove dead code flagged by PMD

PMD caught unused deleteOwnedResourceGroups, deleteLabeledResourceGroups,
stopDeploymentsInResourceGroup, waitForDeploymentsStopped, and
ownsResourceGroup methods left over from moving RG cleanup to CI job.
Also remove unused clearDeploymentIdCache().

* fix(itest): disable stop_deployment tests that kill shared RPT deployment

ActionTest.stop_deployment_changesTargetStatus and DeploymentTest.
update_targetStatus_stopsRunningDeployment stop the RPT deployment
created by AICoreServiceTest. Subsequent Recommendation tests then
hit 404 from the inference endpoint for the full retry budget (~250s)
because the stopped deployment's inference route is gone and the newly
created replacement deployment needs warmup time.

Disable both tests until they are refactored to create and stop their
own isolated deployment instead of the shared one.

* fix(ci): cleanup job reads credentials from VCAP_SERVICES not AICORE_SERVICE_KEY

cds bind --exec injects credentials via VCAP_SERVICES, not
AICORE_SERVICE_KEY. The cleanup script was trying to parse
AICORE_SERVICE_KEY which was undefined, causing a JSON parse error.
…40)

- Replace string concatenation in ServiceException messages with SLF4J-style
  {} placeholders in AICoreSetupHandler (3 locations)
- Replace Collectors.toList() with .toList() in AbstractCrudHandler
- Replace new ArrayList<>() with List.of() for null case
- Remove unused imports (ArrayList, Collectors)

Closes #32
…ler with record (#41)

* fix: use idiomatic patterns for error messages and stream collection

- Replace string concatenation in ServiceException messages with SLF4J-style
  {} placeholders in AICoreSetupHandler (3 locations)
- Replace Collectors.toList() with .toList() in AbstractCrudHandler
- Replace new ArrayList<>() with List.of() for null case
- Remove unused imports (ArrayList, Collectors)

Closes #32

* refactor: use cds-maven-plugin generated interfaces for type-safe entity access

Replace manual AICoreElements constants class with generated typed
interfaces from cds-maven-plugin (following cds-feature-attachments pattern):

- Add cds-maven-plugin generate goal to cds-feature-ai-core/pom.xml
- Generated interfaces: Deployments, Configurations, ResourceGroups,
  BckndResourceGroupLabel (with constants + typed getters/setters)
- Update all handlers to use generated constants (e.g., Deployments.ID)
  and typed factory methods (e.g., Deployments.create(), ResourceGroups.create(id))
- Remove manual AICoreElements.java (replaced by code generation)
- DeploymentHandler: use Deployments.create() with typed setters instead
  of 17-parameter buildDeploymentData method

Closes #33

* fix: use explicit types and remove unnecessary cast in ConfigurationHandler

- Use List<ParameterArgumentBinding> instead of var for bindings
- Use ParameterArgumentBinding instead of var for bm
- Remove unnecessary (CdsData) cast since ParameterArgumentBinding extends CdsData

* fix: use ArtifactArgumentBinding generated interface for inputArtifactBindings

Replace raw CdsData with typed ArtifactArgumentBinding.create() and
setters for the inputArtifactBindings mapping in ConfigurationHandler.

* refactor: use typed data parameters in handler method signatures

Use CAP Java's handler data parameter injection for onCreate/onUpdate:
- DeploymentHandler.onCreate(ctx, List<Deployments>)
- DeploymentHandler.onUpdate(ctx, List<Deployments>)
- ConfigurationHandler.onCreate(ctx, List<Configurations>)
- ResourceGroupHandler.onCreate(ctx, List<ResourceGroups>)

This eliminates manual CQN entry extraction (context.getCqn().entries())
and enables typed getters (entry.getName() vs entry.get("name")).

Update tests to pass data as the second parameter directly.

* fix: remove unused CdsData import in ConfigurationHandler
* fix: use idiomatic patterns for error messages and stream collection

- Replace string concatenation in ServiceException messages with SLF4J-style
  {} placeholders in AICoreSetupHandler (3 locations)
- Replace Collectors.toList() with .toList() in AbstractCrudHandler
- Replace new ArrayList<>() with List.of() for null case
- Remove unused imports (ArrayList, Collectors)

Closes #32

* refactor: use cds-maven-plugin generated interfaces for type-safe entity access

Replace manual AICoreElements constants class with generated typed
interfaces from cds-maven-plugin (following cds-feature-attachments pattern):

- Add cds-maven-plugin generate goal to cds-feature-ai-core/pom.xml
- Generated interfaces: Deployments, Configurations, ResourceGroups,
  BckndResourceGroupLabel (with constants + typed getters/setters)
- Update all handlers to use generated constants (e.g., Deployments.ID)
  and typed factory methods (e.g., Deployments.create(), ResourceGroups.create(id))
- Remove manual AICoreElements.java (replaced by code generation)
- DeploymentHandler: use Deployments.create() with typed setters instead
  of 17-parameter buildDeploymentData method

Closes #33

* refactor: slim AICoreService interface to public API surface

Remove implementation-detail methods from AICoreService interface:
- getDefaultResourceGroup()
- getResourceGroupPrefix()
- getTenantResourceGroupCache()
- getResourceGroupDeploymentCache()
- clearTenantCache()
- resolveResourceGroupFromKeys()

These methods remain public on AICoreServiceImpl and MockAICoreServiceImpl
but are no longer part of the service contract. Only domain-level API
(resourceGroupForTenant, deploymentId, inferenceClient, isMultiTenancyEnabled)
and getRetry() (cross-module consumer) remain on the interface.

Closes #34

* refactor: introduce AbstractAICoreService base class for shared internal methods

Extract getDefaultResourceGroup(), getResourceGroupPrefix(),
getTenantResourceGroupCache(), getResourceGroupDeploymentCache(),
clearTenantCache(), and resolveResourceGroupFromKeys() into an abstract
base class that both AICoreServiceImpl and MockAICoreServiceImpl extend.

This keeps the public AICoreService interface slim (domain API only)
while providing a typed common base for tests and internal consumers
that need access to cache/config methods.

Update all integration and MTX test files to use the new
getAICoreServiceImpl() helper that returns AbstractAICoreService.

Closes #34
The integration-tests-cleanup job now:
- Waits for both integration-tests and sonarqube-scan to finish
- Deletes resource groups with both itest- and sonar- prefixes

Closes #38
)

* fix: use idiomatic patterns for error messages and stream collection

- Replace string concatenation in ServiceException messages with SLF4J-style
  {} placeholders in AICoreSetupHandler (3 locations)
- Replace Collectors.toList() with .toList() in AbstractCrudHandler
- Replace new ArrayList<>() with List.of() for null case
- Remove unused imports (ArrayList, Collectors)

Closes #32

* refactor: use cds-maven-plugin generated interfaces for type-safe entity access

Replace manual AICoreElements constants class with generated typed
interfaces from cds-maven-plugin (following cds-feature-attachments pattern):

- Add cds-maven-plugin generate goal to cds-feature-ai-core/pom.xml
- Generated interfaces: Deployments, Configurations, ResourceGroups,
  BckndResourceGroupLabel (with constants + typed getters/setters)
- Update all handlers to use generated constants (e.g., Deployments.ID)
  and typed factory methods (e.g., Deployments.create(), ResourceGroups.create(id))
- Remove manual AICoreElements.java (replaced by code generation)
- DeploymentHandler: use Deployments.create() with typed setters instead
  of 17-parameter buildDeploymentData method

Closes #33

* refactor: slim AICoreService interface to public API surface

Remove implementation-detail methods from AICoreService interface:
- getDefaultResourceGroup()
- getResourceGroupPrefix()
- getTenantResourceGroupCache()
- getResourceGroupDeploymentCache()
- clearTenantCache()
- resolveResourceGroupFromKeys()

These methods remain public on AICoreServiceImpl and MockAICoreServiceImpl
but are no longer part of the service contract. Only domain-level API
(resourceGroupForTenant, deploymentId, inferenceClient, isMultiTenancyEnabled)
and getRetry() (cross-module consumer) remain on the interface.

Closes #34

* refactor: decompose FioriRecommendationHandler into focused helpers

Extract two helper classes from the 400-line handler:

- RecommendationContextBuilder: entity analysis, prediction element
  discovery, context query building, synthetic key computation, and
  row assembly
- RecommendationResultParser: prediction value type coercion, text path
  resolution from annotations, description DB lookups, and final
  recommendations map assembly

The handler is now a slim orchestrator (~110 lines) that delegates
complex logic to the helpers.

Also replace unbounded ConcurrentHashMap.newKeySet() with a bounded
Caffeine cache (max 10,000 entries) for the entity negative-result cache.

Closes #35

* fix: add explicit caffeine dependency; make computeSyntheticKey private

- Add caffeine dependency to cds-feature-recommendations/pom.xml
  (previously relied on transitive from cds-feature-ai-core)
- Make computeSyntheticKey private in RecommendationContextBuilder
  (only called internally from assembleRows)
* fix: use idiomatic patterns for error messages and stream collection

- Replace string concatenation in ServiceException messages with SLF4J-style
  {} placeholders in AICoreSetupHandler (3 locations)
- Replace Collectors.toList() with .toList() in AbstractCrudHandler
- Replace new ArrayList<>() with List.of() for null case
- Remove unused imports (ArrayList, Collectors)

Closes #32

* refactor: use cds-maven-plugin generated interfaces for type-safe entity access

Replace manual AICoreElements constants class with generated typed
interfaces from cds-maven-plugin (following cds-feature-attachments pattern):

- Add cds-maven-plugin generate goal to cds-feature-ai-core/pom.xml
- Generated interfaces: Deployments, Configurations, ResourceGroups,
  BckndResourceGroupLabel (with constants + typed getters/setters)
- Update all handlers to use generated constants (e.g., Deployments.ID)
  and typed factory methods (e.g., Deployments.create(), ResourceGroups.create(id))
- Remove manual AICoreElements.java (replaced by code generation)
- DeploymentHandler: use Deployments.create() with typed setters instead
  of 17-parameter buildDeploymentData method

Closes #33

* refactor: slim AICoreService interface to public API surface

Remove implementation-detail methods from AICoreService interface:
- getDefaultResourceGroup()
- getResourceGroupPrefix()
- getTenantResourceGroupCache()
- getResourceGroupDeploymentCache()
- clearTenantCache()
- resolveResourceGroupFromKeys()

These methods remain public on AICoreServiceImpl and MockAICoreServiceImpl
but are no longer part of the service contract. Only domain-level API
(resourceGroupForTenant, deploymentId, inferenceClient, isMultiTenancyEnabled)
and getRetry() (cross-module consumer) remain on the interface.

Closes #34

* refactor: decompose FioriRecommendationHandler into focused helpers

Extract two helper classes from the 400-line handler:

- RecommendationContextBuilder: entity analysis, prediction element
  discovery, context query building, synthetic key computation, and
  row assembly
- RecommendationResultParser: prediction value type coercion, text path
  resolution from annotations, description DB lookups, and final
  recommendations map assembly

The handler is now a slim orchestrator (~110 lines) that delegates
complex logic to the helpers.

Also replace unbounded ConcurrentHashMap.newKeySet() with a bounded
Caffeine cache (max 10,000 entries) for the entity negative-result cache.

Closes #35

* refactor: accept SDK API clients as constructor parameters

Add overloaded constructor to AICoreServiceImpl that accepts
DeploymentApi, ConfigurationApi, ResourceGroupApi, and AiCoreService
as parameters. The existing convenience constructor delegates to it
with default instances.

This enables direct injection of mocked clients in unit tests without
needing to mock getter methods on the service itself.

Closes #36

* refactor: remove convenience constructor, pass API clients explicitly

Remove the delegating convenience constructor from AICoreServiceImpl.
The only caller (AICoreServiceConfiguration) now passes DeploymentApi,
ConfigurationApi, ResourceGroupApi, and AiCoreService explicitly,
making the dependency wiring fully transparent at the creation site.

Closes #36
@lisajulia

Copy link
Copy Markdown
Contributor

Some feedback. (I have not looked at the ai core feature in detail so far).

Some general findings:

  • I like the separation into two plugins very much :-)
  • please move the APIs to dedicated api packages
  • looks very tidy overall

AI Core

  • the AICoreService interface could be possibly segragated into

    • the part to manage resource groups and deployment IDs
    • the inference API - maybe you don't need this part at all but could directly register ApiClient as a technical service or Spring bean.
  • the tenant must not appear in the AICoreService API. The user of the AICoreService should be tenant-agnostic but the implementation should be tenant-aware

  • you should provide a mock implementation of the APIClient that can use langchain4j to use local models

  • ideally you externalize the mapping of symbolic inference model names to actual model names so that we can use differently expensive models for testing and in production

Recommendations

  • make sure the annotations can also be added manually
  • consider to register the RecommendationClient as a service so that the application could overwrite the RecommendationClient's on-handler to compute recommendations somehow differently (could be done later)
  • design the RecommendationClient API so that it's not too tightly coupled to RPT-1

Thanks!

* refactor(ai-core): move public API to dedicated api package

Move AICoreService and ModelDeploymentSpec from
com.sap.cds.feature.aicore.core to com.sap.cds.feature.aicore.api so the
public contract is clearly separated from the internal implementation.
Add a package-info.java describing the api package.

Internal classes (AbstractAICoreService, AICoreServiceImpl,
MockAICoreServiceImpl, AICoreServiceConfiguration, handlers) and the
unit test for AICoreServiceImpl get explicit imports for the relocated
types. Implementation references in AICoreService javadoc use FQCN
@link to avoid coupling the interface to internals.

Addresses review feedback on PR #49 (move APIs to api package).

* refactor(recommendations): move public API to dedicated api package

Move RecommendationClient, RecommendationClientResolver, RptInferenceClient
and RptModelSpec from com.sap.cds.feature.recommendation to
com.sap.cds.feature.recommendation.api so the public contract is clearly
separated from the internal implementation. Add a package-info.java for
the api package.

RecommendationClient and RecommendationClientResolver are promoted from
package-private to public to be usable from outside the plugin.

Internal classes (FioriRecommendationHandler, MockRecommendationClient,
RecommendationConfiguration) and tests get explicit imports for the
relocated types.

Addresses review feedback on PR #49 (move APIs to api package).

* test(integration): update imports for relocated api packages

Update test imports in integration-tests/spring and
integration-tests/mtx-local to reference AICoreService, ModelDeploymentSpec
and RptModelSpec from their new api packages.

Addresses review feedback on PR #49 (move APIs to api package).

* samples(bookshop): update imports for relocated api packages

Update AICoreShowcaseHandler imports to reference AICoreService,
RptInferenceClient and RptModelSpec from their new api packages.

Addresses review feedback on PR #49 (move APIs to api package).
Schmarvinius added a commit that referenced this pull request Jun 10, 2026
- Replace resourceGroupForTenant(String) with resourceGroup() on the
  public AICoreService interface. The implementation reads the tenant
  from the current RequestContext internally.
- Remove isMultiTenancyEnabled() and getRetry() from the public
  interface; they remain accessible on AbstractAICoreService for
  internal callers.
- Remove the CDS function 'resourceGroupForTenant' from index.cds
  and its action handler.
- Detect multi-tenancy via standard CAP Java cds.multiTenancy.sidecar.url
  property and DeploymentService presence instead of custom flag.
- Update RecommendationClientResolver to drop tenantId parameter.
- Update samples, tests, and javadoc accordingly.

Addresses review comments from PR #49 (Issue 2).
Schmarvinius added a commit that referenced this pull request Jun 10, 2026
- Add tenant ownership verification on ResourceGroupHandler for
  READ (by-key), UPDATE, and DELETE operations. Returns 404 if the
  resource group belongs to a different tenant.
- Scope list queries (READ without key) to the current tenant's
  resource groups via the tenant label filter in multi-tenancy mode.
- Add ensureResourceGroupAccessible() guard to DeploymentHandler and
  ConfigurationHandler, validating the addressed resource group
  belongs to the current tenant before forwarding to AI Core.
- Provider/system users are exempt from tenant restrictions and can
  access all resource groups (useful for ops/debug scenarios).
- Add isProviderUser() and currentTenantId() as public helpers on
  AbstractAICoreService for use by handler classes.

Addresses review comments from PR #49 (Issue 3a).
Schmarvinius added a commit that referenced this pull request Jun 10, 2026
- Rename all configuration properties from cds.requires.AICore.* to
  cds.ai.core.* to align with CAP Java property naming conventions.
- Rename cds.requires.recommendations.contextRowLimit to
  cds.ai.recommendations.contextRowLimit.
- Drop the cds.requires.AICore.multiTenancy flag entirely; multi-
  tenancy is now auto-detected from standard CAP Java properties.
- Update README with new configuration namespace and examples.

Addresses review comments from PR #49 (Issue 3b).

private static final long MAX_INTERVAL_MS = 30_000L;

private static Retry buildRetry(int maxAttempts, long initialDelayMs) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we add a circuit breaker?

}

@Override
public ApiClient inferenceClient(String resourceGroupId, String deploymentId) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we want to have an integration point for the ai-sdk-java and other higher-level APIs like LangChain4j? How would this look like?

* Make cache for entitiesWithoutPredictionsPerTenant tenant specific

* refactor(ai-core): make AICoreService tenant-agnostic and DI-friendly

- Replace resourceGroupForTenant(String) with resourceGroup() on the
  public AICoreService interface. The implementation reads the tenant
  from the current RequestContext internally.
- Remove isMultiTenancyEnabled() and getRetry() from the public
  interface; they remain accessible on AbstractAICoreService for
  internal callers.
- Remove the CDS function 'resourceGroupForTenant' from index.cds
  and its action handler.
- Detect multi-tenancy via standard CAP Java cds.multiTenancy.sidecar.url
  property and DeploymentService presence instead of custom flag.
- Update RecommendationClientResolver to drop tenantId parameter.
- Update samples, tests, and javadoc accordingly.

Addresses review comments from PR #49 (Issue 2).

* feat(ai-core): restrict AICore entity APIs to current tenant

- Add tenant ownership verification on ResourceGroupHandler for
  READ (by-key), UPDATE, and DELETE operations. Returns 404 if the
  resource group belongs to a different tenant.
- Scope list queries (READ without key) to the current tenant's
  resource groups via the tenant label filter in multi-tenancy mode.
- Add ensureResourceGroupAccessible() guard to DeploymentHandler and
  ConfigurationHandler, validating the addressed resource group
  belongs to the current tenant before forwarding to AI Core.
- Provider/system users are exempt from tenant restrictions and can
  access all resource groups (useful for ops/debug scenarios).
- Add isProviderUser() and currentTenantId() as public helpers on
  AbstractAICoreService for use by handler classes.

Addresses review comments from PR #49 (Issue 3a).

* chore(ai-core): rename config namespace to cds.ai.core

- Rename all configuration properties from cds.requires.AICore.* to
  cds.ai.core.* to align with CAP Java property naming conventions.
- Rename cds.requires.recommendations.contextRowLimit to
  cds.ai.recommendations.contextRowLimit.
- Drop the cds.requires.AICore.multiTenancy flag entirely; multi-
  tenancy is now auto-detected from standard CAP Java properties.
- Update README with new configuration namespace and examples.

Addresses review comments from PR #49 (Issue 3b).

* fix(ai-core): handle null tenant in resourceGroupForTenant

When resourceGroupForTenant is called with a null tenantId (which
happens when currentTenantId() returns null in single-tenant or
non-tenant-scoped RequestContexts), fall back to the default resource
group instead of passing null to the Caffeine cache (which throws NPE).

This fixes integration test failures in the CI pipeline where the
ApplicationServiceDelegation and Recommendation tests run without an
explicit tenant in the RequestContext.

* fix(ci): cleanup all run attempts and cds-itest resource groups

The cleanup step previously only deleted resource groups matching the
exact current run_id AND run_attempt. When a run failed and was re-run,
the previous attempt's resource groups were never cleaned up, eventually
hitting the AI Core resource group limit (50).

Changes:
- Match prefix 'itest-{run_id}-' (all attempts) instead of the exact
  'itest-{run_id}-{run_attempt}' string.
- Same for 'sonar-{run_id}-' prefix.
- Also delete 'cds-itest-' prefixed resource groups which are created
  by the multi-tenancy integration tests via resourceGroupForTenant()
  and were never cleaned up by the pipeline.

* fix(itest): align config namespace with cds.ai.core rename

The source code (commit c30080b) renamed properties from
cds.requires.AICore.* to cds.ai.core.*, but the integration test
application.yaml files were not updated. This meant the
CDS_AICORE_TEST_RESOURCE_GROUP env var set by CI was silently ignored
and tests always ran against the literal default resource group.

- spring/application.yaml: cds.requires.AICore -> cds.ai.core
- mtx-local/application.yaml: remove obsolete cds.requires.AICore.multiTenancy
  (now auto-detected from cds.multi-tenancy.sidecar.url)

* test(ai-core): add unit tests for tenant scoping and mock service

Cover new code paths introduced by the tenant-scoping branch:

- TenantScopingTest (7 tests): exercises every branch of
  AbstractCrudHandler.ensureResourceGroupAccessible() — provider bypass,
  single-tenancy bypass, null tenant, matching/non-matching labels, 404.

- MockAICoreServiceImplTest (9 tests): both constructors,
  MT enabled/disabled, resourceGroupForTenant, cache isolation,
  clearTenantCache, getRetry, config property reads.

- AICoreServiceImplDeploymentIdTest (+2 tests):
  resourceGroupForTenant(null) returns default even with MT enabled;
  single-tenancy always returns default.

* chore(recommendations): add TODO for model-changed integration test

Document the missing E2E coverage for RecommendationModelChangedHandler.
The proper test requires an extensibility-enabled sidecar with extension
JSON that adds prediction columns — not yet set up in mtx-local.

The cache-invalidation logic itself is covered by the existing unit test
FioriRecommendationHandlerTest.invalidateTenant_removesOnlyThatTenantsEntries.

* update cleanup

* fix(ci): scope resource group cleanup to own job only

Each parallel CI job (Java 17, Java 21, SonarQube) was using broad
prefixes in its cleanup step, deleting resource groups belonging to
sibling jobs still in progress. This caused intermittent 403 Forbidden
errors when the affected jobs tried to use their now-deleted resource
groups.

Narrow the cleanup prefixes so each job only deletes its own:
- integration-tests: itest-{run_id}-{attempt}-j{version}*
- scan-with-sonar: sonar-{run_id}-{attempt}*
Both still clean up itest-rg-* (ResourceGroupTest leftovers).

* test(ai-core): add unit tests for uncovered code paths

- DeploymentHandler: test onCreate (with/without TTL) and onUpdate
  happy path (targetStatus and configurationId branches)
- ResourceGroupHandler: test onUpdate with/without labels,
  buildTenantLabelSelector branches (tenantId filter, MT non-provider,
  MT null tenant, single tenancy), ensureOwnedByCurrentTenant branches
  (provider, single tenant, wrong tenant, matching tenant)
- AICoreServiceConfiguration: test eventHandlers() MockAICoreServiceImpl
  branch (with and without multi-tenancy), test detectMultiTenancy via
  services() for sidecarUrl branch and no-MT fallback

* fix(ci): include cds-itest- prefix in resource group cleanup

The MultiTenancyTest creates per-tenant resource groups with names like
cds-itest-mt-a-{timestamp} (from resourceGroupPrefix 'cds-' + tenant
name 'itest-*'). These are unique per test run (timestamped) and safe
to clean up from any job without cross-job interference.

* refactor: extract inline cleanup script into shared JS file

---------

Co-authored-by: Marvin L <marvin.lindner@sap.com>
Schmarvinius added a commit that referenced this pull request Jun 15, 2026
* Make cache for entitiesWithoutPredictionsPerTenant tenant specific

* refactor(ai-core): make AICoreService tenant-agnostic and DI-friendly

- Replace resourceGroupForTenant(String) with resourceGroup() on the
  public AICoreService interface. The implementation reads the tenant
  from the current RequestContext internally.
- Remove isMultiTenancyEnabled() and getRetry() from the public
  interface; they remain accessible on AbstractAICoreService for
  internal callers.
- Remove the CDS function 'resourceGroupForTenant' from index.cds
  and its action handler.
- Detect multi-tenancy via standard CAP Java cds.multiTenancy.sidecar.url
  property and DeploymentService presence instead of custom flag.
- Update RecommendationClientResolver to drop tenantId parameter.
- Update samples, tests, and javadoc accordingly.

Addresses review comments from PR #49 (Issue 2).

* feat(ai-core): restrict AICore entity APIs to current tenant

- Add tenant ownership verification on ResourceGroupHandler for
  READ (by-key), UPDATE, and DELETE operations. Returns 404 if the
  resource group belongs to a different tenant.
- Scope list queries (READ without key) to the current tenant's
  resource groups via the tenant label filter in multi-tenancy mode.
- Add ensureResourceGroupAccessible() guard to DeploymentHandler and
  ConfigurationHandler, validating the addressed resource group
  belongs to the current tenant before forwarding to AI Core.
- Provider/system users are exempt from tenant restrictions and can
  access all resource groups (useful for ops/debug scenarios).
- Add isProviderUser() and currentTenantId() as public helpers on
  AbstractAICoreService for use by handler classes.

Addresses review comments from PR #49 (Issue 3a).

* chore(ai-core): rename config namespace to cds.ai.core

- Rename all configuration properties from cds.requires.AICore.* to
  cds.ai.core.* to align with CAP Java property naming conventions.
- Rename cds.requires.recommendations.contextRowLimit to
  cds.ai.recommendations.contextRowLimit.
- Drop the cds.requires.AICore.multiTenancy flag entirely; multi-
  tenancy is now auto-detected from standard CAP Java properties.
- Update README with new configuration namespace and examples.

Addresses review comments from PR #49 (Issue 3b).

* fix(ai-core): handle null tenant in resourceGroupForTenant

When resourceGroupForTenant is called with a null tenantId (which
happens when currentTenantId() returns null in single-tenant or
non-tenant-scoped RequestContexts), fall back to the default resource
group instead of passing null to the Caffeine cache (which throws NPE).

This fixes integration test failures in the CI pipeline where the
ApplicationServiceDelegation and Recommendation tests run without an
explicit tenant in the RequestContext.

* fix(ci): cleanup all run attempts and cds-itest resource groups

The cleanup step previously only deleted resource groups matching the
exact current run_id AND run_attempt. When a run failed and was re-run,
the previous attempt's resource groups were never cleaned up, eventually
hitting the AI Core resource group limit (50).

Changes:
- Match prefix 'itest-{run_id}-' (all attempts) instead of the exact
  'itest-{run_id}-{run_attempt}' string.
- Same for 'sonar-{run_id}-' prefix.
- Also delete 'cds-itest-' prefixed resource groups which are created
  by the multi-tenancy integration tests via resourceGroupForTenant()
  and were never cleaned up by the pipeline.

* fix(itest): align config namespace with cds.ai.core rename

The source code (commit c30080b) renamed properties from
cds.requires.AICore.* to cds.ai.core.*, but the integration test
application.yaml files were not updated. This meant the
CDS_AICORE_TEST_RESOURCE_GROUP env var set by CI was silently ignored
and tests always ran against the literal default resource group.

- spring/application.yaml: cds.requires.AICore -> cds.ai.core
- mtx-local/application.yaml: remove obsolete cds.requires.AICore.multiTenancy
  (now auto-detected from cds.multi-tenancy.sidecar.url)

* test(ai-core): add unit tests for tenant scoping and mock service

Cover new code paths introduced by the tenant-scoping branch:

- TenantScopingTest (7 tests): exercises every branch of
  AbstractCrudHandler.ensureResourceGroupAccessible() — provider bypass,
  single-tenancy bypass, null tenant, matching/non-matching labels, 404.

- MockAICoreServiceImplTest (9 tests): both constructors,
  MT enabled/disabled, resourceGroupForTenant, cache isolation,
  clearTenantCache, getRetry, config property reads.

- AICoreServiceImplDeploymentIdTest (+2 tests):
  resourceGroupForTenant(null) returns default even with MT enabled;
  single-tenancy always returns default.

* chore(recommendations): add TODO for model-changed integration test

Document the missing E2E coverage for RecommendationModelChangedHandler.
The proper test requires an extensibility-enabled sidecar with extension
JSON that adds prediction columns — not yet set up in mtx-local.

The cache-invalidation logic itself is covered by the existing unit test
FioriRecommendationHandlerTest.invalidateTenant_removesOnlyThatTenantsEntries.

* update cleanup

* fix(ci): scope resource group cleanup to own job only

Each parallel CI job (Java 17, Java 21, SonarQube) was using broad
prefixes in its cleanup step, deleting resource groups belonging to
sibling jobs still in progress. This caused intermittent 403 Forbidden
errors when the affected jobs tried to use their now-deleted resource
groups.

Narrow the cleanup prefixes so each job only deletes its own:
- integration-tests: itest-{run_id}-{attempt}-j{version}*
- scan-with-sonar: sonar-{run_id}-{attempt}*
Both still clean up itest-rg-* (ResourceGroupTest leftovers).

* test(ai-core): add unit tests for uncovered code paths

- DeploymentHandler: test onCreate (with/without TTL) and onUpdate
  happy path (targetStatus and configurationId branches)
- ResourceGroupHandler: test onUpdate with/without labels,
  buildTenantLabelSelector branches (tenantId filter, MT non-provider,
  MT null tenant, single tenancy), ensureOwnedByCurrentTenant branches
  (provider, single tenant, wrong tenant, matching tenant)
- AICoreServiceConfiguration: test eventHandlers() MockAICoreServiceImpl
  branch (with and without multi-tenancy), test detectMultiTenancy via
  services() for sidecarUrl branch and no-MT fallback

* fix(ci): include cds-itest- prefix in resource group cleanup

The MultiTenancyTest creates per-tenant resource groups with names like
cds-itest-mt-a-{timestamp} (from resourceGroupPrefix 'cds-' + tenant
name 'itest-*'). These are unique per test run (timestamped) and safe
to clean up from any job without cross-job interference.

* refactor(ai-core): migrate AICoreService from CqnService to RemoteService

* refactor(ai-core): delete AICoreApplicationServiceHandler

* fix(ai-core): guard service registration on AICore model presence

* fix(test): mock CdsModel in unit tests and restore AICore model import

* fix(ai-core): extend AbstractCdsDefinedService for proper RemoteService support

* fix(recommendations): promote cds-services-impl to compile scope

* refactor(test): use real CdsRuntime in AICoreServiceConfigurationTest

* refactor(ai-core): remove AICORE_SERVICE_KEY env var check from binding detection

* chore: exclude Mock* classes from SonarQube coverage

* refactor(test): use real CdsRuntime in AICoreServiceImplDeploymentIdTest

* fix: remove duplicate detectMultiTenancy method from merge

---------

Co-authored-by: Lisa Julia Nebel <lisa.nebel@sap.com>
Schmarvinius added a commit that referenced this pull request Jun 15, 2026
* Make cache for entitiesWithoutPredictionsPerTenant tenant specific

* refactor(ai-core): make AICoreService tenant-agnostic and DI-friendly

- Replace resourceGroupForTenant(String) with resourceGroup() on the
  public AICoreService interface. The implementation reads the tenant
  from the current RequestContext internally.
- Remove isMultiTenancyEnabled() and getRetry() from the public
  interface; they remain accessible on AbstractAICoreService for
  internal callers.
- Remove the CDS function 'resourceGroupForTenant' from index.cds
  and its action handler.
- Detect multi-tenancy via standard CAP Java cds.multiTenancy.sidecar.url
  property and DeploymentService presence instead of custom flag.
- Update RecommendationClientResolver to drop tenantId parameter.
- Update samples, tests, and javadoc accordingly.

Addresses review comments from PR #49 (Issue 2).

* feat(ai-core): restrict AICore entity APIs to current tenant

- Add tenant ownership verification on ResourceGroupHandler for
  READ (by-key), UPDATE, and DELETE operations. Returns 404 if the
  resource group belongs to a different tenant.
- Scope list queries (READ without key) to the current tenant's
  resource groups via the tenant label filter in multi-tenancy mode.
- Add ensureResourceGroupAccessible() guard to DeploymentHandler and
  ConfigurationHandler, validating the addressed resource group
  belongs to the current tenant before forwarding to AI Core.
- Provider/system users are exempt from tenant restrictions and can
  access all resource groups (useful for ops/debug scenarios).
- Add isProviderUser() and currentTenantId() as public helpers on
  AbstractAICoreService for use by handler classes.

Addresses review comments from PR #49 (Issue 3a).

* chore(ai-core): rename config namespace to cds.ai.core

- Rename all configuration properties from cds.requires.AICore.* to
  cds.ai.core.* to align with CAP Java property naming conventions.
- Rename cds.requires.recommendations.contextRowLimit to
  cds.ai.recommendations.contextRowLimit.
- Drop the cds.requires.AICore.multiTenancy flag entirely; multi-
  tenancy is now auto-detected from standard CAP Java properties.
- Update README with new configuration namespace and examples.

Addresses review comments from PR #49 (Issue 3b).

* fix(ai-core): handle null tenant in resourceGroupForTenant

When resourceGroupForTenant is called with a null tenantId (which
happens when currentTenantId() returns null in single-tenant or
non-tenant-scoped RequestContexts), fall back to the default resource
group instead of passing null to the Caffeine cache (which throws NPE).

This fixes integration test failures in the CI pipeline where the
ApplicationServiceDelegation and Recommendation tests run without an
explicit tenant in the RequestContext.

* fix(ci): cleanup all run attempts and cds-itest resource groups

The cleanup step previously only deleted resource groups matching the
exact current run_id AND run_attempt. When a run failed and was re-run,
the previous attempt's resource groups were never cleaned up, eventually
hitting the AI Core resource group limit (50).

Changes:
- Match prefix 'itest-{run_id}-' (all attempts) instead of the exact
  'itest-{run_id}-{run_attempt}' string.
- Same for 'sonar-{run_id}-' prefix.
- Also delete 'cds-itest-' prefixed resource groups which are created
  by the multi-tenancy integration tests via resourceGroupForTenant()
  and were never cleaned up by the pipeline.

* fix(itest): align config namespace with cds.ai.core rename

The source code (commit c30080b) renamed properties from
cds.requires.AICore.* to cds.ai.core.*, but the integration test
application.yaml files were not updated. This meant the
CDS_AICORE_TEST_RESOURCE_GROUP env var set by CI was silently ignored
and tests always ran against the literal default resource group.

- spring/application.yaml: cds.requires.AICore -> cds.ai.core
- mtx-local/application.yaml: remove obsolete cds.requires.AICore.multiTenancy
  (now auto-detected from cds.multi-tenancy.sidecar.url)

* test(ai-core): add unit tests for tenant scoping and mock service

Cover new code paths introduced by the tenant-scoping branch:

- TenantScopingTest (7 tests): exercises every branch of
  AbstractCrudHandler.ensureResourceGroupAccessible() — provider bypass,
  single-tenancy bypass, null tenant, matching/non-matching labels, 404.

- MockAICoreServiceImplTest (9 tests): both constructors,
  MT enabled/disabled, resourceGroupForTenant, cache isolation,
  clearTenantCache, getRetry, config property reads.

- AICoreServiceImplDeploymentIdTest (+2 tests):
  resourceGroupForTenant(null) returns default even with MT enabled;
  single-tenancy always returns default.

* chore(recommendations): add TODO for model-changed integration test

Document the missing E2E coverage for RecommendationModelChangedHandler.
The proper test requires an extensibility-enabled sidecar with extension
JSON that adds prediction columns — not yet set up in mtx-local.

The cache-invalidation logic itself is covered by the existing unit test
FioriRecommendationHandlerTest.invalidateTenant_removesOnlyThatTenantsEntries.

* update cleanup

* fix(ci): scope resource group cleanup to own job only

Each parallel CI job (Java 17, Java 21, SonarQube) was using broad
prefixes in its cleanup step, deleting resource groups belonging to
sibling jobs still in progress. This caused intermittent 403 Forbidden
errors when the affected jobs tried to use their now-deleted resource
groups.

Narrow the cleanup prefixes so each job only deletes its own:
- integration-tests: itest-{run_id}-{attempt}-j{version}*
- scan-with-sonar: sonar-{run_id}-{attempt}*
Both still clean up itest-rg-* (ResourceGroupTest leftovers).

* test(ai-core): add unit tests for uncovered code paths

- DeploymentHandler: test onCreate (with/without TTL) and onUpdate
  happy path (targetStatus and configurationId branches)
- ResourceGroupHandler: test onUpdate with/without labels,
  buildTenantLabelSelector branches (tenantId filter, MT non-provider,
  MT null tenant, single tenancy), ensureOwnedByCurrentTenant branches
  (provider, single tenant, wrong tenant, matching tenant)
- AICoreServiceConfiguration: test eventHandlers() MockAICoreServiceImpl
  branch (with and without multi-tenancy), test detectMultiTenancy via
  services() for sidecarUrl branch and no-MT fallback

* fix(ci): include cds-itest- prefix in resource group cleanup

The MultiTenancyTest creates per-tenant resource groups with names like
cds-itest-mt-a-{timestamp} (from resourceGroupPrefix 'cds-' + tenant
name 'itest-*'). These are unique per test run (timestamped) and safe
to clean up from any job without cross-job interference.

* refactor(ai-core): migrate AICoreService from CqnService to RemoteService

* refactor(ai-core): delete AICoreApplicationServiceHandler

* fix(ai-core): guard service registration on AICore model presence

* fix(test): mock CdsModel in unit tests and restore AICore model import

* fix(ai-core): extend AbstractCdsDefinedService for proper RemoteService support

* fix(recommendations): promote cds-services-impl to compile scope

* refactor(test): use real CdsRuntime in AICoreServiceConfigurationTest

* refactor(ai-core): remove AICORE_SERVICE_KEY env var check from binding detection

* chore: exclude Mock* classes from SonarQube coverage

* refactor(test): use real CdsRuntime in AICoreServiceImplDeploymentIdTest

* fix: remove duplicate detectMultiTenancy method from merge

* refactor(ai-core): rename DEFAULT_NAME to AICoreService$Default

Follow standard CAP Java naming convention for service instances
(ServiceInterface$Default). The CDS definition name stays 'AICore'
(matching the CDS model); only the registered instance name changes.

* refactor(ai-core): define EventContext subinterfaces for programmatic API

Add typed EventContext interfaces in the api package for the three
programmatic API methods:
- DeploymentIdContext: for deploymentId(resourceGroupId, spec)
- InferenceClientContext: for inferenceClient(resourceGroupId, deploymentId)
- ResourceGroupContext: for resourceGroup()

These enable the idiomatic CAP pattern where service methods emit events
and ON handlers provide the implementation, allowing extensibility via
@Before/@after hooks.

* refactor(ai-core): make service API methods emit events; move logic to handler

Apply idiomatic CAP Java pattern: service methods create typed EventContext,
emit(), and return result. A separately-registered AICoreApiHandler provides
the ON implementation with the actual business logic.

- AICoreServiceImpl.deploymentId/inferenceClient/resourceGroup/
  resourceGroupForTenant now emit typed contexts instead of doing work directly
- New AICoreApiHandler handles DeploymentIdContext, InferenceClientContext,
  ResourceGroupContext with all caching, retry, and SDK logic
- ResourceGroupContext extended with optional tenantId for explicit-tenant path
- AICoreServiceConfiguration registers AICoreApiHandler
- AICoreServiceImpl retains shared state (caches, config, APIs) accessed by
  handlers via EventContext.getService()

This enables extensibility: apps can register @Before/@after handlers on
deploymentId, inferenceClient, and resourceGroup events.

* refactor(ai-core): decouple CRUD handlers from service impl; use typed contexts

- Remove AICoreServiceImpl field from AbstractCrudHandler; handlers now
  obtain the service from EventContext.getService() at invocation time.
- Pass SDK API clients (DeploymentApi, ResourceGroupApi, ConfigurationApi)
  directly via constructor injection — stateless clients don't need the
  service reference.
- ActionHandler uses generated DeploymentsStopContext instead of raw
  EventContext with string-based key extraction.
- All handlers use generated entity name constants (Deployments_.CDS_NAME,
  ResourceGroups_.CDS_NAME, Configurations_.CDS_NAME) instead of
  hand-written strings.
- Update AICoreServiceConfiguration to pass API clients to handler
  constructors.
- Update all handler unit tests for the new constructor signatures and
  EventContext-based service access pattern.

Addresses issue #70: typesafe handlers decoupled from service impl.

* refactor(ai-core): remove redundant event params from handler annotations

* test(ai-core): rewrite handler tests to use real CdsRuntime

Replace heavily-mocked unit tests with integration-style tests that boot
a real CdsRuntime, register real handlers, and dispatch CQN through the
full handler pipeline. Only SDK API clients remain mocked.

- DeploymentHandlerTest: tests CREATE, UPDATE via service.run()
- ConfigurationHandlerTest: tests READ, CREATE via service.run()
- ResourceGroupHandlerTest: tests CRUD + MT label filtering
- TenantScopingTest: tests tenant isolation through actual CQN operations
  with different RequestContext tenants

* refactor(ai-core): extract AICoreConfig and AICoreClients

Immutable record for config values and holder for SDK API clients.

* refactor(ai-core): extract DeploymentResolver

Encapsulates caches, locks, retry, and validation behind
resolveResourceGroup, resolveDeployment, invalidateTenant.

* refactor(ai-core): inject components into handlers

Handlers receive dependencies via constructor. Use
context.getUserInfo() for tenant/provider checks directly.

* refactor(ai-core): slim AICoreServiceImpl to pure delegation

Zero fields, zero accessors. Delete AbstractAICoreService and
MockAICoreServiceImpl. Add resourceGroupForTenant to interface.

* refactor(ai-core): rewire configuration and setup handlers

Configuration creates AICoreConfig, AICoreClients, DeploymentResolver
and injects them into handlers at registration time.

* refactor(recommendations): RptInferenceClient owns its retry

Single-arg constructor, no dependency on service internals.
Remove AbstractAICoreService casts from all consumers.

* test(ai-core): update tests for new component architecture

Adapt all unit and integration tests to use AICoreConfig,
AICoreClients, DeploymentResolver instead of service accessors.

* for pipeline

* fix(ai-core): separate retry boundary to prevent orphaned deployments

Only retry the deployment creation call. Polling is handled
separately so a poll timeout does not re-create deployments.

* refactor(ai-core): remove all service references from handlers

Handlers use DeploymentResolver.resolveResourceGroup() directly
instead of casting context.getService(). Zero service references.

* fix(ai-core): add handler ordering and wire mock cleanup

Add @HandlerOrder to setup handlers for DeploymentService events.
Wire MockAICoreSetupHandler to actually call clearTenantCache().
Use ServiceException in mock inference handler.

* fix(ai-core): validate config at startup, document impl coupling

Fail fast on invalid cds.ai.core.* property values.
Document AbstractCdsDefinedService dependency rationale.

* test(ai-core): update tests for DeploymentResolver expansion

Pass ResourceGroupApi to DeploymentResolver constructor.
Pass DeploymentResolver to CRUD handler constructors.

---------

Co-authored-by: Lisa Julia Nebel <lisa.nebel@sap.com>
* pipeline

* refactor(ai-core): migrate AICoreService to RemoteService (#73)

* Make cache for entitiesWithoutPredictionsPerTenant tenant specific

* refactor(ai-core): make AICoreService tenant-agnostic and DI-friendly

- Replace resourceGroupForTenant(String) with resourceGroup() on the
  public AICoreService interface. The implementation reads the tenant
  from the current RequestContext internally.
- Remove isMultiTenancyEnabled() and getRetry() from the public
  interface; they remain accessible on AbstractAICoreService for
  internal callers.
- Remove the CDS function 'resourceGroupForTenant' from index.cds
  and its action handler.
- Detect multi-tenancy via standard CAP Java cds.multiTenancy.sidecar.url
  property and DeploymentService presence instead of custom flag.
- Update RecommendationClientResolver to drop tenantId parameter.
- Update samples, tests, and javadoc accordingly.

Addresses review comments from PR #49 (Issue 2).

* feat(ai-core): restrict AICore entity APIs to current tenant

- Add tenant ownership verification on ResourceGroupHandler for
  READ (by-key), UPDATE, and DELETE operations. Returns 404 if the
  resource group belongs to a different tenant.
- Scope list queries (READ without key) to the current tenant's
  resource groups via the tenant label filter in multi-tenancy mode.
- Add ensureResourceGroupAccessible() guard to DeploymentHandler and
  ConfigurationHandler, validating the addressed resource group
  belongs to the current tenant before forwarding to AI Core.
- Provider/system users are exempt from tenant restrictions and can
  access all resource groups (useful for ops/debug scenarios).
- Add isProviderUser() and currentTenantId() as public helpers on
  AbstractAICoreService for use by handler classes.

Addresses review comments from PR #49 (Issue 3a).

* chore(ai-core): rename config namespace to cds.ai.core

- Rename all configuration properties from cds.requires.AICore.* to
  cds.ai.core.* to align with CAP Java property naming conventions.
- Rename cds.requires.recommendations.contextRowLimit to
  cds.ai.recommendations.contextRowLimit.
- Drop the cds.requires.AICore.multiTenancy flag entirely; multi-
  tenancy is now auto-detected from standard CAP Java properties.
- Update README with new configuration namespace and examples.

Addresses review comments from PR #49 (Issue 3b).

* fix(ai-core): handle null tenant in resourceGroupForTenant

When resourceGroupForTenant is called with a null tenantId (which
happens when currentTenantId() returns null in single-tenant or
non-tenant-scoped RequestContexts), fall back to the default resource
group instead of passing null to the Caffeine cache (which throws NPE).

This fixes integration test failures in the CI pipeline where the
ApplicationServiceDelegation and Recommendation tests run without an
explicit tenant in the RequestContext.

* fix(ci): cleanup all run attempts and cds-itest resource groups

The cleanup step previously only deleted resource groups matching the
exact current run_id AND run_attempt. When a run failed and was re-run,
the previous attempt's resource groups were never cleaned up, eventually
hitting the AI Core resource group limit (50).

Changes:
- Match prefix 'itest-{run_id}-' (all attempts) instead of the exact
  'itest-{run_id}-{run_attempt}' string.
- Same for 'sonar-{run_id}-' prefix.
- Also delete 'cds-itest-' prefixed resource groups which are created
  by the multi-tenancy integration tests via resourceGroupForTenant()
  and were never cleaned up by the pipeline.

* fix(itest): align config namespace with cds.ai.core rename

The source code (commit c30080b) renamed properties from
cds.requires.AICore.* to cds.ai.core.*, but the integration test
application.yaml files were not updated. This meant the
CDS_AICORE_TEST_RESOURCE_GROUP env var set by CI was silently ignored
and tests always ran against the literal default resource group.

- spring/application.yaml: cds.requires.AICore -> cds.ai.core
- mtx-local/application.yaml: remove obsolete cds.requires.AICore.multiTenancy
  (now auto-detected from cds.multi-tenancy.sidecar.url)

* test(ai-core): add unit tests for tenant scoping and mock service

Cover new code paths introduced by the tenant-scoping branch:

- TenantScopingTest (7 tests): exercises every branch of
  AbstractCrudHandler.ensureResourceGroupAccessible() — provider bypass,
  single-tenancy bypass, null tenant, matching/non-matching labels, 404.

- MockAICoreServiceImplTest (9 tests): both constructors,
  MT enabled/disabled, resourceGroupForTenant, cache isolation,
  clearTenantCache, getRetry, config property reads.

- AICoreServiceImplDeploymentIdTest (+2 tests):
  resourceGroupForTenant(null) returns default even with MT enabled;
  single-tenancy always returns default.

* chore(recommendations): add TODO for model-changed integration test

Document the missing E2E coverage for RecommendationModelChangedHandler.
The proper test requires an extensibility-enabled sidecar with extension
JSON that adds prediction columns — not yet set up in mtx-local.

The cache-invalidation logic itself is covered by the existing unit test
FioriRecommendationHandlerTest.invalidateTenant_removesOnlyThatTenantsEntries.

* update cleanup

* fix(ci): scope resource group cleanup to own job only

Each parallel CI job (Java 17, Java 21, SonarQube) was using broad
prefixes in its cleanup step, deleting resource groups belonging to
sibling jobs still in progress. This caused intermittent 403 Forbidden
errors when the affected jobs tried to use their now-deleted resource
groups.

Narrow the cleanup prefixes so each job only deletes its own:
- integration-tests: itest-{run_id}-{attempt}-j{version}*
- scan-with-sonar: sonar-{run_id}-{attempt}*
Both still clean up itest-rg-* (ResourceGroupTest leftovers).

* test(ai-core): add unit tests for uncovered code paths

- DeploymentHandler: test onCreate (with/without TTL) and onUpdate
  happy path (targetStatus and configurationId branches)
- ResourceGroupHandler: test onUpdate with/without labels,
  buildTenantLabelSelector branches (tenantId filter, MT non-provider,
  MT null tenant, single tenancy), ensureOwnedByCurrentTenant branches
  (provider, single tenant, wrong tenant, matching tenant)
- AICoreServiceConfiguration: test eventHandlers() MockAICoreServiceImpl
  branch (with and without multi-tenancy), test detectMultiTenancy via
  services() for sidecarUrl branch and no-MT fallback

* fix(ci): include cds-itest- prefix in resource group cleanup

The MultiTenancyTest creates per-tenant resource groups with names like
cds-itest-mt-a-{timestamp} (from resourceGroupPrefix 'cds-' + tenant
name 'itest-*'). These are unique per test run (timestamped) and safe
to clean up from any job without cross-job interference.

* refactor(ai-core): migrate AICoreService from CqnService to RemoteService

* refactor(ai-core): delete AICoreApplicationServiceHandler

* fix(ai-core): guard service registration on AICore model presence

* fix(test): mock CdsModel in unit tests and restore AICore model import

* fix(ai-core): extend AbstractCdsDefinedService for proper RemoteService support

* fix(recommendations): promote cds-services-impl to compile scope

* refactor(test): use real CdsRuntime in AICoreServiceConfigurationTest

* refactor(ai-core): remove AICORE_SERVICE_KEY env var check from binding detection

* chore: exclude Mock* classes from SonarQube coverage

* refactor(test): use real CdsRuntime in AICoreServiceImplDeploymentIdTest

* fix: remove duplicate detectMultiTenancy method from merge

---------

Co-authored-by: Lisa Julia Nebel <lisa.nebel@sap.com>

* refactor(ai-core): typesafe handlers decoupled from service impl (#74)

* Make cache for entitiesWithoutPredictionsPerTenant tenant specific

* refactor(ai-core): make AICoreService tenant-agnostic and DI-friendly

- Replace resourceGroupForTenant(String) with resourceGroup() on the
  public AICoreService interface. The implementation reads the tenant
  from the current RequestContext internally.
- Remove isMultiTenancyEnabled() and getRetry() from the public
  interface; they remain accessible on AbstractAICoreService for
  internal callers.
- Remove the CDS function 'resourceGroupForTenant' from index.cds
  and its action handler.
- Detect multi-tenancy via standard CAP Java cds.multiTenancy.sidecar.url
  property and DeploymentService presence instead of custom flag.
- Update RecommendationClientResolver to drop tenantId parameter.
- Update samples, tests, and javadoc accordingly.

Addresses review comments from PR #49 (Issue 2).

* feat(ai-core): restrict AICore entity APIs to current tenant

- Add tenant ownership verification on ResourceGroupHandler for
  READ (by-key), UPDATE, and DELETE operations. Returns 404 if the
  resource group belongs to a different tenant.
- Scope list queries (READ without key) to the current tenant's
  resource groups via the tenant label filter in multi-tenancy mode.
- Add ensureResourceGroupAccessible() guard to DeploymentHandler and
  ConfigurationHandler, validating the addressed resource group
  belongs to the current tenant before forwarding to AI Core.
- Provider/system users are exempt from tenant restrictions and can
  access all resource groups (useful for ops/debug scenarios).
- Add isProviderUser() and currentTenantId() as public helpers on
  AbstractAICoreService for use by handler classes.

Addresses review comments from PR #49 (Issue 3a).

* chore(ai-core): rename config namespace to cds.ai.core

- Rename all configuration properties from cds.requires.AICore.* to
  cds.ai.core.* to align with CAP Java property naming conventions.
- Rename cds.requires.recommendations.contextRowLimit to
  cds.ai.recommendations.contextRowLimit.
- Drop the cds.requires.AICore.multiTenancy flag entirely; multi-
  tenancy is now auto-detected from standard CAP Java properties.
- Update README with new configuration namespace and examples.

Addresses review comments from PR #49 (Issue 3b).

* fix(ai-core): handle null tenant in resourceGroupForTenant

When resourceGroupForTenant is called with a null tenantId (which
happens when currentTenantId() returns null in single-tenant or
non-tenant-scoped RequestContexts), fall back to the default resource
group instead of passing null to the Caffeine cache (which throws NPE).

This fixes integration test failures in the CI pipeline where the
ApplicationServiceDelegation and Recommendation tests run without an
explicit tenant in the RequestContext.

* fix(ci): cleanup all run attempts and cds-itest resource groups

The cleanup step previously only deleted resource groups matching the
exact current run_id AND run_attempt. When a run failed and was re-run,
the previous attempt's resource groups were never cleaned up, eventually
hitting the AI Core resource group limit (50).

Changes:
- Match prefix 'itest-{run_id}-' (all attempts) instead of the exact
  'itest-{run_id}-{run_attempt}' string.
- Same for 'sonar-{run_id}-' prefix.
- Also delete 'cds-itest-' prefixed resource groups which are created
  by the multi-tenancy integration tests via resourceGroupForTenant()
  and were never cleaned up by the pipeline.

* fix(itest): align config namespace with cds.ai.core rename

The source code (commit c30080b) renamed properties from
cds.requires.AICore.* to cds.ai.core.*, but the integration test
application.yaml files were not updated. This meant the
CDS_AICORE_TEST_RESOURCE_GROUP env var set by CI was silently ignored
and tests always ran against the literal default resource group.

- spring/application.yaml: cds.requires.AICore -> cds.ai.core
- mtx-local/application.yaml: remove obsolete cds.requires.AICore.multiTenancy
  (now auto-detected from cds.multi-tenancy.sidecar.url)

* test(ai-core): add unit tests for tenant scoping and mock service

Cover new code paths introduced by the tenant-scoping branch:

- TenantScopingTest (7 tests): exercises every branch of
  AbstractCrudHandler.ensureResourceGroupAccessible() — provider bypass,
  single-tenancy bypass, null tenant, matching/non-matching labels, 404.

- MockAICoreServiceImplTest (9 tests): both constructors,
  MT enabled/disabled, resourceGroupForTenant, cache isolation,
  clearTenantCache, getRetry, config property reads.

- AICoreServiceImplDeploymentIdTest (+2 tests):
  resourceGroupForTenant(null) returns default even with MT enabled;
  single-tenancy always returns default.

* chore(recommendations): add TODO for model-changed integration test

Document the missing E2E coverage for RecommendationModelChangedHandler.
The proper test requires an extensibility-enabled sidecar with extension
JSON that adds prediction columns — not yet set up in mtx-local.

The cache-invalidation logic itself is covered by the existing unit test
FioriRecommendationHandlerTest.invalidateTenant_removesOnlyThatTenantsEntries.

* update cleanup

* fix(ci): scope resource group cleanup to own job only

Each parallel CI job (Java 17, Java 21, SonarQube) was using broad
prefixes in its cleanup step, deleting resource groups belonging to
sibling jobs still in progress. This caused intermittent 403 Forbidden
errors when the affected jobs tried to use their now-deleted resource
groups.

Narrow the cleanup prefixes so each job only deletes its own:
- integration-tests: itest-{run_id}-{attempt}-j{version}*
- scan-with-sonar: sonar-{run_id}-{attempt}*
Both still clean up itest-rg-* (ResourceGroupTest leftovers).

* test(ai-core): add unit tests for uncovered code paths

- DeploymentHandler: test onCreate (with/without TTL) and onUpdate
  happy path (targetStatus and configurationId branches)
- ResourceGroupHandler: test onUpdate with/without labels,
  buildTenantLabelSelector branches (tenantId filter, MT non-provider,
  MT null tenant, single tenancy), ensureOwnedByCurrentTenant branches
  (provider, single tenant, wrong tenant, matching tenant)
- AICoreServiceConfiguration: test eventHandlers() MockAICoreServiceImpl
  branch (with and without multi-tenancy), test detectMultiTenancy via
  services() for sidecarUrl branch and no-MT fallback

* fix(ci): include cds-itest- prefix in resource group cleanup

The MultiTenancyTest creates per-tenant resource groups with names like
cds-itest-mt-a-{timestamp} (from resourceGroupPrefix 'cds-' + tenant
name 'itest-*'). These are unique per test run (timestamped) and safe
to clean up from any job without cross-job interference.

* refactor(ai-core): migrate AICoreService from CqnService to RemoteService

* refactor(ai-core): delete AICoreApplicationServiceHandler

* fix(ai-core): guard service registration on AICore model presence

* fix(test): mock CdsModel in unit tests and restore AICore model import

* fix(ai-core): extend AbstractCdsDefinedService for proper RemoteService support

* fix(recommendations): promote cds-services-impl to compile scope

* refactor(test): use real CdsRuntime in AICoreServiceConfigurationTest

* refactor(ai-core): remove AICORE_SERVICE_KEY env var check from binding detection

* chore: exclude Mock* classes from SonarQube coverage

* refactor(test): use real CdsRuntime in AICoreServiceImplDeploymentIdTest

* fix: remove duplicate detectMultiTenancy method from merge

* refactor(ai-core): rename DEFAULT_NAME to AICoreService$Default

Follow standard CAP Java naming convention for service instances
(ServiceInterface$Default). The CDS definition name stays 'AICore'
(matching the CDS model); only the registered instance name changes.

* refactor(ai-core): define EventContext subinterfaces for programmatic API

Add typed EventContext interfaces in the api package for the three
programmatic API methods:
- DeploymentIdContext: for deploymentId(resourceGroupId, spec)
- InferenceClientContext: for inferenceClient(resourceGroupId, deploymentId)
- ResourceGroupContext: for resourceGroup()

These enable the idiomatic CAP pattern where service methods emit events
and ON handlers provide the implementation, allowing extensibility via
@Before/@after hooks.

* refactor(ai-core): make service API methods emit events; move logic to handler

Apply idiomatic CAP Java pattern: service methods create typed EventContext,
emit(), and return result. A separately-registered AICoreApiHandler provides
the ON implementation with the actual business logic.

- AICoreServiceImpl.deploymentId/inferenceClient/resourceGroup/
  resourceGroupForTenant now emit typed contexts instead of doing work directly
- New AICoreApiHandler handles DeploymentIdContext, InferenceClientContext,
  ResourceGroupContext with all caching, retry, and SDK logic
- ResourceGroupContext extended with optional tenantId for explicit-tenant path
- AICoreServiceConfiguration registers AICoreApiHandler
- AICoreServiceImpl retains shared state (caches, config, APIs) accessed by
  handlers via EventContext.getService()

This enables extensibility: apps can register @Before/@after handlers on
deploymentId, inferenceClient, and resourceGroup events.

* refactor(ai-core): decouple CRUD handlers from service impl; use typed contexts

- Remove AICoreServiceImpl field from AbstractCrudHandler; handlers now
  obtain the service from EventContext.getService() at invocation time.
- Pass SDK API clients (DeploymentApi, ResourceGroupApi, ConfigurationApi)
  directly via constructor injection — stateless clients don't need the
  service reference.
- ActionHandler uses generated DeploymentsStopContext instead of raw
  EventContext with string-based key extraction.
- All handlers use generated entity name constants (Deployments_.CDS_NAME,
  ResourceGroups_.CDS_NAME, Configurations_.CDS_NAME) instead of
  hand-written strings.
- Update AICoreServiceConfiguration to pass API clients to handler
  constructors.
- Update all handler unit tests for the new constructor signatures and
  EventContext-based service access pattern.

Addresses issue #70: typesafe handlers decoupled from service impl.

* refactor(ai-core): remove redundant event params from handler annotations

* test(ai-core): rewrite handler tests to use real CdsRuntime

Replace heavily-mocked unit tests with integration-style tests that boot
a real CdsRuntime, register real handlers, and dispatch CQN through the
full handler pipeline. Only SDK API clients remain mocked.

- DeploymentHandlerTest: tests CREATE, UPDATE via service.run()
- ConfigurationHandlerTest: tests READ, CREATE via service.run()
- ResourceGroupHandlerTest: tests CRUD + MT label filtering
- TenantScopingTest: tests tenant isolation through actual CQN operations
  with different RequestContext tenants

* refactor(ai-core): extract AICoreConfig and AICoreClients

Immutable record for config values and holder for SDK API clients.

* refactor(ai-core): extract DeploymentResolver

Encapsulates caches, locks, retry, and validation behind
resolveResourceGroup, resolveDeployment, invalidateTenant.

* refactor(ai-core): inject components into handlers

Handlers receive dependencies via constructor. Use
context.getUserInfo() for tenant/provider checks directly.

* refactor(ai-core): slim AICoreServiceImpl to pure delegation

Zero fields, zero accessors. Delete AbstractAICoreService and
MockAICoreServiceImpl. Add resourceGroupForTenant to interface.

* refactor(ai-core): rewire configuration and setup handlers

Configuration creates AICoreConfig, AICoreClients, DeploymentResolver
and injects them into handlers at registration time.

* refactor(recommendations): RptInferenceClient owns its retry

Single-arg constructor, no dependency on service internals.
Remove AbstractAICoreService casts from all consumers.

* test(ai-core): update tests for new component architecture

Adapt all unit and integration tests to use AICoreConfig,
AICoreClients, DeploymentResolver instead of service accessors.

* for pipeline

* fix(ai-core): separate retry boundary to prevent orphaned deployments

Only retry the deployment creation call. Polling is handled
separately so a poll timeout does not re-create deployments.

* refactor(ai-core): remove all service references from handlers

Handlers use DeploymentResolver.resolveResourceGroup() directly
instead of casting context.getService(). Zero service references.

* fix(ai-core): add handler ordering and wire mock cleanup

Add @HandlerOrder to setup handlers for DeploymentService events.
Wire MockAICoreSetupHandler to actually call clearTenantCache().
Use ServiceException in mock inference handler.

* fix(ai-core): validate config at startup, document impl coupling

Fail fast on invalid cds.ai.core.* property values.
Document AbstractCdsDefinedService dependency rationale.

* test(ai-core): update tests for DeploymentResolver expansion

Pass ResourceGroupApi to DeploymentResolver constructor.
Pass DeploymentResolver to CRUD handler constructors.

---------

Co-authored-by: Lisa Julia Nebel <lisa.nebel@sap.com>

* chore(ai-core): cleanup (#76)

* move handler to proper package

* cleanup not relevant params in Before/After annotation

* fix local test

* Refactor cds-feature-recommendations (#77)

* Separate the predictionRow from the contextRows and add a constant for [PREDICT]

* Add comment on why we the @FunctionalInterface annotation is useful

* Add comment to FioriRecommendationHandler

* Moved everthing RPT-1 specific into RptInferenceClient

* Rename RptInferenceClient.api -> RptInferenceClient.rpt and extract code that creates sdkRow to separate method

* Moved everthing RPT-1 specific into RptInferenceClient

* Replace MANAGED_FIELDS set with annotation-driven exclusion: @Core.Computed, @readonly and remove logic from 'toSdkRow' method that was already executed elsewhere

* Change level of log statement when no suitable context columns are found and recommendations are therefore skipped

* Move 'return early if predictRow == null' to earlier in afterRead of FioriRecommendationHandler

* Extract 'SAP_Recommendations' into a constant

* Get Persistence Service db via Dependency Injection

* Move missingPredictionElementNames to earlier in the afterRead of the FioriRecommendationHandler

* Add comment about conversion from List<Row> to List<CdsData>

* Change check for active Entity: only return early if isActiveEntity is selected and false

* Add comments to MockRecommendationClient

* Change type of slectcolumns to Set

* Add test: row for which we want to do predictions is automatically excluded in the select query returned by buildContextQuery

* Add comment about ordering in the select query returned by buildContextQuery

* Add comment computeSyntheticKey method and add test

* Make sure we dont react on @cds.odata.valuelist : false and add a test for that

* Simplify filters in computeContextColumn

* Use any single key as RPT-1 index column, not just fields called ID and add test

* Adjust sample to changes in cds-feature-recomendations

* Add comment about books entity in test model

* Update javadoc

* Do not register FioriRecommendationHandler if no PersistenceService is found

* In RptInferenceClient:resolveIndexColumn: fall back to synthetic index column for non-string single keys

* Add checks for the presence of keyNames

* Minor changes

* Remove reflection tests for resolveIndexColumn

The index column resolution is also tested by nonIdKey_usesSyntheticKeyColumn
and composedKeys_usesSyntheticKeyColumn in FioriRecommendationHandlerTest.

* Extract RptIndexColumns utility to share index column logic between RptInferenceClient and MockRecommendationClient

* Add synthetic Key (if needed) in the sdkRow creation

* Refactor: remove keyNames from RecommendationClient interface — it is now an argument of the Resolver via RecommendationClientResolver<T>

* chore: migrate to remote service (#80)

* refactor(ai-core): migrate AICoreService to RemoteService

- Remove AICoreService interface and AICoreServiceImpl
- Introduce AICore constants class with SERVICE_NAME
- Convert all handlers to use RemoteService with event contexts
- Update DeploymentIdContext, InferenceClientContext, ResourceGroupContext
- Update AICoreServiceConfiguration to register handlers on RemoteService
- Adapt all unit tests to new RemoteService-based API
- Update sample .cdsrc.json and ai-core-service.cds

* chore(itests): adapt integration tests to RemoteService

- Update all integration tests to use RemoteService + event contexts
- Replace AICoreService.deploymentId/resourceGroup calls with context pattern
- Update BaseIntegrationTest with new service resolution approach

* refactor(recommendations): adapt to RemoteService API

- Remove RptIndexColumns utility; inline resolveIndexColumn and
  addSyntheticKeyIfNeeded into RptInferenceClient
- Change RecommendationClient.predict to accept keyNames as argument
- Drop generic type parameter from RecommendationClientResolver;
  resolve now takes RemoteService directly
- Remove keyNames from RptInferenceClient constructor (passed at predict time)
- FioriRecommendationHandler now holds RemoteService reference and passes
  keyNames at prediction time
- RecommendationConfiguration uses RemoteService + event context pattern
  (ResourceGroupContext, DeploymentIdContext, InferenceClientContext)
- MockRecommendationClient simplified (no keyNames in constructor)
- Update all tests to match new signatures

* chore(samples): adapt bookshop sample to RemoteService

- Replace AICoreService usage with RemoteService + event context pattern
- Use AICore.SERVICE_NAME constant for service lookup
- Demonstrate ResourceGroupContext, DeploymentIdContext, InferenceClientContext

* update last Cqn references

* update unit-tests

* restore functionality

* adapt tests

* simplify itests

* simplification

* spotless

* big blunder now fixed whoops

* last occurences

* spotless

* feature: static recommendation state (#82)

* refactor(ai-core): migrate AICoreService to RemoteService

- Remove AICoreService interface and AICoreServiceImpl
- Introduce AICore constants class with SERVICE_NAME
- Convert all handlers to use RemoteService with event contexts
- Update DeploymentIdContext, InferenceClientContext, ResourceGroupContext
- Update AICoreServiceConfiguration to register handlers on RemoteService
- Adapt all unit tests to new RemoteService-based API
- Update sample .cdsrc.json and ai-core-service.cds

* chore(itests): adapt integration tests to RemoteService

- Update all integration tests to use RemoteService + event contexts
- Replace AICoreService.deploymentId/resourceGroup calls with context pattern
- Update BaseIntegrationTest with new service resolution approach

* refactor(recommendations): adapt to RemoteService API

- Remove RptIndexColumns utility; inline resolveIndexColumn and
  addSyntheticKeyIfNeeded into RptInferenceClient
- Change RecommendationClient.predict to accept keyNames as argument
- Drop generic type parameter from RecommendationClientResolver;
  resolve now takes RemoteService directly
- Remove keyNames from RptInferenceClient constructor (passed at predict time)
- FioriRecommendationHandler now holds RemoteService reference and passes
  keyNames at prediction time
- RecommendationConfiguration uses RemoteService + event context pattern
  (ResourceGroupContext, DeploymentIdContext, InferenceClientContext)
- MockRecommendationClient simplified (no keyNames in constructor)
- Update all tests to match new signatures

* chore(samples): adapt bookshop sample to RemoteService

- Replace AICoreService usage with RemoteService + event context pattern
- Use AICore.SERVICE_NAME constant for service lookup
- Demonstrate ResourceGroupContext, DeploymentIdContext, InferenceClientContext

* update last Cqn references

* update unit-tests

* restore functionality

* adapt tests

* simplify itests

* simplification

* spotless

* big blunder now fixed whoops

* last occurences

* add filter

* readme update

* test adaption

* cleanup

* update readme

---------

Signed-off-by: Marvin L <marvin.lindner@sap.com>

* Final review doc changes (#84)

* Document how to add the SAP_Recommendations navigation property manually

* Add @odata.draft.enabled in README.md

* Mark RPT-1 specific setting as such in README

* Add Integer64 and UUID to the supported-types table

* Drop Prerequisites section and include the info from there throughout the README

* Changes to recommendations-test.cds

* Remove 'detect automatically via ServiceBindingUtils'

* fix recommendation unit tests (revert)

* revert pr pipeline branches

* continue-on-error

* Fix one sonarqube issue apart from the coverage (#85)

* simplification

* adapt version

---------

Signed-off-by: Marvin L <marvin.lindner@sap.com>
Co-authored-by: Lisa Julia Nebel <lisa.nebel@sap.com>
* add missing info

* adapt versions
dependabot Bot and others added 3 commits June 19, 2026 10:44
…ates (#81)

Bumps the minor-patch group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [com.sap.cloud.sdk:sdk-bom](https://github.com/SAP/cloud-sdk-java) | `5.30.0` | `5.31.0` |
| [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) | `0.8.14` | `0.8.15` |
| [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) | `3.5.5` | `3.5.6` |
| [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) | `3.5.5` | `3.5.6` |
| [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) | `4.9.8.3` | `4.10.2.0` |
| [com.diffplug.spotless:spotless-maven-plugin](https://github.com/diffplug/spotless) | `3.5.1` | `3.6.0` |



Updates `com.sap.cloud.sdk:sdk-bom` from 5.30.0 to 5.31.0
- [Release notes](https://github.com/SAP/cloud-sdk-java/releases)
- [Changelog](https://github.com/SAP/cloud-sdk-java/blob/main/release_notes.md)
- [Commits](SAP/cloud-sdk-java@rel/5.30.0...rel/5.31.0)

Updates `org.jacoco:jacoco-maven-plugin` from 0.8.14 to 0.8.15
- [Release notes](https://github.com/jacoco/jacoco/releases)
- [Commits](jacoco/jacoco@v0.8.14...v0.8.15)

Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.5 to 3.5.6
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](apache/maven-surefire@surefire-3.5.5...surefire-3.5.6)

Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.5.5 to 3.5.6
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](apache/maven-surefire@surefire-3.5.5...surefire-3.5.6)

Updates `com.github.spotbugs:spotbugs-maven-plugin` from 4.9.8.3 to 4.10.2.0
- [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases)
- [Commits](spotbugs/spotbugs-maven-plugin@spotbugs-maven-plugin-4.9.8.3...spotbugs-maven-plugin-4.10.2.0)

Updates `com.diffplug.spotless:spotless-maven-plugin` from 3.5.1 to 3.6.0
- [Release notes](https://github.com/diffplug/spotless/releases)
- [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md)
- [Commits](diffplug/spotless@maven/3.5.1...maven/3.6.0)

---
updated-dependencies:
- dependency-name: com.diffplug.spotless:spotless-maven-plugin
  dependency-version: 3.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: com.github.spotbugs:spotbugs-maven-plugin
  dependency-version: 4.10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: com.sap.cloud.sdk:sdk-bom
  dependency-version: 5.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: org.apache.maven.plugins:maven-failsafe-plugin
  dependency-version: 3.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: org.jacoco:jacoco-maven-plugin
  dependency-version: 0.8.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the minor-patch group with 1 update in the / directory: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@de0fac2...df4cb1c)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marvin L <marvin.lindner@sap.com>
@Schmarvinius Schmarvinius deployed to release-approval June 19, 2026 13:12 — with GitHub Actions Active
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.

5 participants