From 10f70a11f9d5fbcc7bb754799be340d2366cdc64 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Fri, 22 May 2026 20:14:18 +0200 Subject: [PATCH] fix: filter recommendations to unfilled fields only (#22) 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. --- .../FioriRecommendationHandler.java | 4 +- .../FioriRecommendationHandlerTest.java | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/FioriRecommendationHandler.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/FioriRecommendationHandler.java index f6ae25c..46aef51 100644 --- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/FioriRecommendationHandler.java +++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/FioriRecommendationHandler.java @@ -215,8 +215,10 @@ public void afterRead(CdsReadEventContext context, List dataList) { return; } + List missingPredictionElementNames = + predictionElementNames.stream().filter(c -> row.get(c) == null).toList(); Map recommendations = - buildRecommendations(db, predictions.get(0), predictionElementNames, context, rowType); + buildRecommendations(db, predictions.get(0), missingPredictionElementNames, context, rowType); row.put("SAP_Recommendations", recommendations); } diff --git a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java index cdb4158..7e46d4b 100644 --- a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java +++ b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java @@ -271,6 +271,31 @@ void composedKeys_usesSyntheticKeyColumn() { }); } + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + void rptStyleClient_filledColumns_areExcludedFromRecommendations() { + runIn( + () -> { + Map row = new HashMap<>(); + row.put("ID", "a009c640-434a-4542-ac68-51b400c880ec"); + row.put("IsActiveEntity", false); + row.put("genre_ID", 12); + row.put("currency_code", null); + CdsReadEventContext ctx = readContext("test.Books", List.of(row)); + when(db.run(any(CqnSelect.class))) + .thenReturn( + twoContextRows(), + ResultBuilder.selectedRows(List.of()).result(), + ResultBuilder.selectedRows(List.of()).result()); + predictionClient = rptStyleClient(); + cut.afterRead(ctx, dataList(row)); + assertThat(row).containsKey("SAP_Recommendations"); + Map recs = (Map) row.get("SAP_Recommendations"); + assertThat(recs).doesNotContainKey("genre_ID"); + assertThat((List) recs.get("currency_code")).hasSize(1); + }); + } + // ── helpers ──────────────────────────────────────────────────────────────── private CdsReadEventContext readContext(String entityName, List> resultRows) { @@ -317,6 +342,31 @@ private static Result twoContextRows() { .result(); } + private static RecommendationClient rptStyleClient() { + Random random = new Random(42); + return (rows, predictionColumns, indexColumn) -> { + List predictions = new ArrayList<>(); + for (CdsData row : rows) { + if (predictionColumns.stream().noneMatch(col -> "[PREDICT]".equals(row.get(col)))) { + continue; + } + Map prediction = new HashMap<>(); + for (String col : predictionColumns) { + List available = + rows.stream() + .filter(r -> r.get(col) != null && !"[PREDICT]".equals(r.get(col))) + .map(r -> r.get(col)) + .toList(); + Object val = available.isEmpty() ? null : available.get(random.nextInt(available.size())); + prediction.put(col, List.of(Map.of("prediction", val))); + } + prediction.put(indexColumn, row.get(indexColumn)); + predictions.add(CdsData.create(prediction)); + } + return predictions; + }; + } + private static RecommendationClient randomPickClient() { Random random = new Random(42); return (rows, predictionColumns, indexColumn) -> {