From 27cfaee00805b97d738820bb6f9e267ece5a05e3 Mon Sep 17 00:00:00 2001 From: hywznn Date: Tue, 18 Aug 2026 10:42:06 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(ai-run):=20=EA=B7=BC=EB=A1=9C=EC=9E=90?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EB=8B=A8=EC=9D=BC=20=EC=A0=91=EB=91=90?= =?UTF-8?q?=EC=96=B4=20=EC=A1=B0=ED=9A=8C=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/JpaWorkerAiContextReader.java | 14 +++++- .../AiSlotResolutionIntegrationTest.java | 49 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerAiContextReader.java b/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerAiContextReader.java index 179ba029..ccb38323 100644 --- a/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerAiContextReader.java +++ b/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerAiContextReader.java @@ -70,7 +70,7 @@ and not exists ( } catch (IllegalArgumentException ignored) { return List.of(); } - List workerIds = entityManager.createQuery( + List candidateRows = entityManager.createQuery( """ select worker.workerId, worker.displayName from WorkerJpaEntity worker @@ -86,12 +86,22 @@ and not exists ( Object[].class ) .setParameter("companyId", companyId) - .getResultList() + .getResultList(); + List workerIds = candidateRows .stream() .filter(row -> lookupKey.equals(displayNameNormalizer.normalize((String) row[1]))) .limit(2) .map(row -> (UUID) row[0]) .toList(); + if (workerIds.isEmpty() && lookupKey.codePointCount(0, lookupKey.length()) >= 3) { + workerIds = candidateRows + .stream() + .filter(row -> displayNameNormalizer.normalize((String) row[1]) + .startsWith(lookupKey)) + .limit(2) + .map(row -> (UUID) row[0]) + .toList(); + } if (workerIds.isEmpty()) { return List.of(); } diff --git a/src/test/java/com/fowoco/server/airun/AiSlotResolutionIntegrationTest.java b/src/test/java/com/fowoco/server/airun/AiSlotResolutionIntegrationTest.java index 7a32a167..5ea051db 100644 --- a/src/test/java/com/fowoco/server/airun/AiSlotResolutionIntegrationTest.java +++ b/src/test/java/com/fowoco/server/airun/AiSlotResolutionIntegrationTest.java @@ -148,6 +148,55 @@ void multipleNormalizedCandidatesAreReportedAsAmbiguous() { ); } + @Test + void resolvesUniqueNormalizedPrefixInsideTheCurrentCompany() { + insertCompany(COMPANY_A, "사업장 A"); + insertWorker(WORKER_A, COMPANY_A, "응웬반A", "2026-09-30"); + + AiSlotResolution result = resolutionTransaction.resolve( + COMPANY_A, + "0.3.1", + requirement("응웬반") + ); + + assertThat(result.worker().workerRef()).isEqualTo(WORKER_A); + } + + @Test + void multipleNormalizedPrefixCandidatesAreReportedAsAmbiguous() { + insertCompany(COMPANY_A, "사업장 A"); + insertWorker(WORKER_A, COMPANY_A, "응웬반A", "2026-09-30"); + insertWorker(WORKER_A_DUPLICATE, COMPANY_A, "응웬반B", "2027-09-30"); + + assertThatThrownBy(() -> resolutionTransaction.resolve( + COMPANY_A, + "0.3.1", + requirement("응웬반") + )) + .isInstanceOfSatisfying(AiContextResolutionException.class, exception -> + assertThat(exception.failureCode()).isEqualTo( + AiContextResolutionFailureCode.TARGET_AMBIGUOUS + ) + ); + } + + @Test + void shortPrefixIsNotUsedForWorkerResolution() { + insertCompany(COMPANY_A, "사업장 A"); + insertWorker(WORKER_A, COMPANY_A, "응웬반A", "2026-09-30"); + + assertThatThrownBy(() -> resolutionTransaction.resolve( + COMPANY_A, + "0.3.1", + requirement("응") + )) + .isInstanceOfSatisfying(AiContextResolutionException.class, exception -> + assertThat(exception.failureCode()).isEqualTo( + AiContextResolutionFailureCode.TARGET_NOT_FOUND + ) + ); + } + @Test void separatorOnlyTargetIsReportedAsNotFound() { insertCompany(COMPANY_A, "사업장 A"); From 1188d669a3d04de55255179d606f62441a479649 Mon Sep 17 00:00:00 2001 From: hywznn Date: Tue, 18 Aug 2026 11:48:08 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(ai-run):=20=EB=AC=B8=EC=9E=A5=ED=98=95?= =?UTF-8?q?=20=EA=B7=BC=EB=A1=9C=EC=9E=90=20=EB=8C=80=EC=83=81=EB=AA=85=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/JpaWorkerAiContextReader.java | 23 +++++++++++++++ .../AiSlotResolutionIntegrationTest.java | 29 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerAiContextReader.java b/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerAiContextReader.java index ccb38323..6df2f5e1 100644 --- a/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerAiContextReader.java +++ b/src/main/java/com/fowoco/server/worker/infrastructure/persistence/JpaWorkerAiContextReader.java @@ -102,6 +102,29 @@ and not exists ( .map(row -> (UUID) row[0]) .toList(); } + if (workerIds.isEmpty()) { + List sentencePrefixCandidates = candidateRows.stream() + .filter(row -> { + String candidateKey = displayNameNormalizer.normalize((String) row[1]); + return candidateKey.codePointCount(0, candidateKey.length()) >= 3 + && lookupKey.startsWith(candidateKey); + }) + .toList(); + int longestCandidateLength = sentencePrefixCandidates.stream() + .map(row -> displayNameNormalizer.normalize((String) row[1])) + .mapToInt(value -> value.codePointCount(0, value.length())) + .max() + .orElse(0); + workerIds = sentencePrefixCandidates.stream() + .filter(row -> { + String candidateKey = displayNameNormalizer.normalize((String) row[1]); + return candidateKey.codePointCount(0, candidateKey.length()) + == longestCandidateLength; + }) + .limit(2) + .map(row -> (UUID) row[0]) + .toList(); + } if (workerIds.isEmpty()) { return List.of(); } diff --git a/src/test/java/com/fowoco/server/airun/AiSlotResolutionIntegrationTest.java b/src/test/java/com/fowoco/server/airun/AiSlotResolutionIntegrationTest.java index 5ea051db..0c6a9862 100644 --- a/src/test/java/com/fowoco/server/airun/AiSlotResolutionIntegrationTest.java +++ b/src/test/java/com/fowoco/server/airun/AiSlotResolutionIntegrationTest.java @@ -162,6 +162,35 @@ void resolvesUniqueNormalizedPrefixInsideTheCurrentCompany() { assertThat(result.worker().workerRef()).isEqualTo(WORKER_A); } + @Test + void resolvesWorkerWhenAgentReturnsSentenceStartingWithTheDisplayName() { + insertCompany(COMPANY_A, "사업장 A"); + insertWorker(WORKER_A, COMPANY_A, "응웬반A", "2026-09-30"); + + AiSlotResolution result = resolutionTransaction.resolve( + COMPANY_A, + "0.3.1", + requirement("응웬반A가 3년 만료 예정이야.") + ); + + assertThat(result.worker().workerRef()).isEqualTo(WORKER_A); + } + + @Test + void longestDisplayNameWinsWhenAgentReturnsANamePrefixedSentence() { + insertCompany(COMPANY_A, "사업장 A"); + insertWorker(WORKER_A, COMPANY_A, "응웬반", "2026-09-30"); + insertWorker(WORKER_A_DUPLICATE, COMPANY_A, "응웬반A", "2027-09-30"); + + AiSlotResolution result = resolutionTransaction.resolve( + COMPANY_A, + "0.3.1", + requirement("응웬반A가 3년 만료 예정이야.") + ); + + assertThat(result.worker().workerRef()).isEqualTo(WORKER_A_DUPLICATE); + } + @Test void multipleNormalizedPrefixCandidatesAreReportedAsAmbiguous() { insertCompany(COMPANY_A, "사업장 A");