Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 671ebf5fea | |||
| 7fe1e2e4ee | |||
| 0a5a216032 | |||
| 4be32afe13 | |||
| 1aee39c1ad | |||
| e31825021c |
@@ -49,6 +49,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
module:
|
module:
|
||||||
- account
|
- account
|
||||||
|
- analysis
|
||||||
- bot-platform
|
- bot-platform
|
||||||
- coordinator
|
- coordinator
|
||||||
- core
|
- core
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ val coverageExclusions = listOf(
|
|||||||
"**/core/src/main/scala/de/nowchess/chess/resource/GameWebSocketResource.scala",
|
"**/core/src/main/scala/de/nowchess/chess/resource/GameWebSocketResource.scala",
|
||||||
// Coordinator infrastructure — gRPC, microservice orchestration
|
// Coordinator infrastructure — gRPC, microservice orchestration
|
||||||
"**/coordinator/src/main/scala/**",
|
"**/coordinator/src/main/scala/**",
|
||||||
|
// Analysis resource/config — REST integration layer; @QuarkusTest not instrumented by Scoverage
|
||||||
|
"**/analysis/src/main/scala/de/nowchess/analysis/resource/**",
|
||||||
|
"**/analysis/src/main/scala/de/nowchess/analysis/config/**",
|
||||||
|
"**/analysis/src/main/scala/de/nowchess/analysis/error/AnalysisExceptionMapper.scala",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Converts a Sonar-style glob to a scoverage regex (matched against full source path).
|
// Converts a Sonar-style glob to a scoverage regex (matched against full source path).
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Changelog — analysis
|
||||||
|
|
||||||
|
## 0.1.0 (NCS-71)
|
||||||
|
|
||||||
|
- Initial scaffold: chess analysis microservice
|
||||||
|
- REST endpoint `POST /api/analysis/position` wrapping chess-api.com
|
||||||
|
- REST endpoint `GET /api/analysis/health`
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
plugins {
|
||||||
|
id("scala")
|
||||||
|
id("org.scoverage") version "8.1"
|
||||||
|
id("io.quarkus")
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "de.nowchess"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val versions = rootProject.extra["VERSIONS"] as Map<String, String>
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val scoverageExcluded = rootProject.extra["SCOVERAGE_EXCLUDED"] as List<String>
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
scala {
|
||||||
|
scalaVersion = versions["SCALA3"]!!
|
||||||
|
}
|
||||||
|
|
||||||
|
scoverage {
|
||||||
|
scoverageVersion.set(versions["SCOVERAGE"]!!)
|
||||||
|
excludedFiles.set(scoverageExcluded)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<ScalaCompile> {
|
||||||
|
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
val quarkusPlatformGroupId: String by project
|
||||||
|
val quarkusPlatformArtifactId: String by project
|
||||||
|
val quarkusPlatformVersion: String by project
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly("org.scala-lang:scala3-compiler_3") {
|
||||||
|
version {
|
||||||
|
strictly(versions["SCALA3"]!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
implementation("org.scala-lang:scala3-library_3") {
|
||||||
|
version {
|
||||||
|
strictly(versions["SCALA3"]!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
|
||||||
|
implementation("io.quarkus:quarkus-rest")
|
||||||
|
implementation("io.quarkus:quarkus-rest-client")
|
||||||
|
implementation("io.quarkus:quarkus-rest-client-jackson")
|
||||||
|
implementation("io.quarkus:quarkus-rest-jackson")
|
||||||
|
implementation("io.quarkus:quarkus-config-yaml")
|
||||||
|
implementation("io.quarkus:quarkus-smallrye-fault-tolerance")
|
||||||
|
implementation("io.quarkus:quarkus-smallrye-health")
|
||||||
|
implementation("io.quarkus:quarkus-logging-json")
|
||||||
|
implementation("io.quarkus:quarkus-micrometer")
|
||||||
|
implementation("io.quarkus:quarkus-micrometer-registry-prometheus")
|
||||||
|
implementation("io.quarkus:quarkus-opentelemetry")
|
||||||
|
implementation("io.quarkus:quarkus-arc")
|
||||||
|
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
|
||||||
|
|
||||||
|
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
||||||
|
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
|
||||||
|
testImplementation("io.quarkus:quarkus-junit5")
|
||||||
|
testImplementation("io.quarkus:quarkus-junit5-mockito")
|
||||||
|
testImplementation("io.rest-assured:rest-assured")
|
||||||
|
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations.matching { !it.name.startsWith("scoverage") }.configureEach {
|
||||||
|
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
|
||||||
|
}
|
||||||
|
configurations.scoverage {
|
||||||
|
resolutionStrategy.eachDependency {
|
||||||
|
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
|
||||||
|
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<JavaCompile> {
|
||||||
|
options.encoding = "UTF-8"
|
||||||
|
options.compilerArgs.add("-parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Jar>().configureEach {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform {
|
||||||
|
includeEngines("scalatest", "junit-jupiter")
|
||||||
|
testLogging {
|
||||||
|
events("passed", "skipped", "failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalizedBy(tasks.reportScoverage)
|
||||||
|
}
|
||||||
|
tasks.reportScoverage {
|
||||||
|
dependsOn(tasks.test)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.jar {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
####
|
||||||
|
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
|
||||||
|
#
|
||||||
|
# Before building the container image run:
|
||||||
|
#
|
||||||
|
# ./gradlew :modules:analysis:build -Dquarkus.native.enabled=true
|
||||||
|
#
|
||||||
|
# Then, build the image with:
|
||||||
|
#
|
||||||
|
# docker build -f src/main/docker/Dockerfile.native -t quarkus/backcore .
|
||||||
|
#
|
||||||
|
# Then run the container using:
|
||||||
|
#
|
||||||
|
# docker run -i --rm -p 8087:8087 quarkus/backcore
|
||||||
|
#
|
||||||
|
# The `registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9.
|
||||||
|
# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`.
|
||||||
|
###
|
||||||
|
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7
|
||||||
|
WORKDIR /work/
|
||||||
|
RUN chown 1001 /work \
|
||||||
|
&& chmod "g+rwX" /work \
|
||||||
|
&& chown 1001:root /work
|
||||||
|
COPY --chown=1001:root --chmod=0755 modules/analysis/build/*-runner /work/application
|
||||||
|
|
||||||
|
EXPOSE 8087
|
||||||
|
USER 1001
|
||||||
|
|
||||||
|
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
quarkus:
|
||||||
|
http:
|
||||||
|
port: 8087
|
||||||
|
application:
|
||||||
|
name: nowchess-analysis
|
||||||
|
config:
|
||||||
|
yaml:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
nowchess:
|
||||||
|
analysis:
|
||||||
|
chess-api:
|
||||||
|
base-url: ${CHESS_API_URL:https://chess-api.com/v1}
|
||||||
|
timeout-ms: ${CHESS_API_TIMEOUT_MS:5000}
|
||||||
|
|
||||||
|
"%dev":
|
||||||
|
quarkus:
|
||||||
|
rest-client:
|
||||||
|
chess-api:
|
||||||
|
url: https://chess-api.com/v1
|
||||||
|
connect-timeout: 5000
|
||||||
|
read-timeout: 5000
|
||||||
|
|
||||||
|
"%deployed":
|
||||||
|
quarkus:
|
||||||
|
log:
|
||||||
|
console:
|
||||||
|
json: true
|
||||||
|
otel:
|
||||||
|
traces:
|
||||||
|
sampler: parentbased_traceidratio
|
||||||
|
sampler-arg: 0.1
|
||||||
|
exporter:
|
||||||
|
otlp:
|
||||||
|
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}
|
||||||
|
rest-client:
|
||||||
|
chess-api:
|
||||||
|
url: ${CHESS_API_URL:https://chess-api.com/v1}
|
||||||
|
connect-timeout: ${CHESS_API_CONNECT_TIMEOUT_MS:5000}
|
||||||
|
read-timeout: ${CHESS_API_TIMEOUT_MS:5000}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.nowchess.analysis.client
|
||||||
|
|
||||||
|
import jakarta.ws.rs.*
|
||||||
|
import jakarta.ws.rs.core.MediaType
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||||
|
|
||||||
|
/** MicroProfile REST client for chess-api.com v1.
|
||||||
|
*
|
||||||
|
* Base URL is resolved from `quarkus.rest-client.chess-api.url` in application.yml.
|
||||||
|
*/
|
||||||
|
@Path("/")
|
||||||
|
@RegisterRestClient(configKey = "chess-api")
|
||||||
|
trait ChessApiClient:
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
|
def analyse(body: ChessApiRequestDto): ChessApiResponseDto
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package de.nowchess.analysis.client
|
||||||
|
|
||||||
|
/** Request body sent to chess-api.com v1 `/` endpoint. */
|
||||||
|
case class ChessApiRequestDto(fen: String, depth: Int)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package de.nowchess.analysis.client
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
/** Response from chess-api.com v1 analysis endpoint.
|
||||||
|
*
|
||||||
|
* The API returns a JSON object. Fields not listed here are ignored.
|
||||||
|
*/
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
case class ChessApiResponseDto(
|
||||||
|
/** Best move in UCI format (e.g. "e2e4"). */
|
||||||
|
move: Option[String] = None,
|
||||||
|
/** Centipawn evaluation (from white's perspective). */
|
||||||
|
centipawns: Option[Double] = None,
|
||||||
|
/** Mate-in-N (positive = white wins, negative = black wins). */
|
||||||
|
mate: Option[Int] = None,
|
||||||
|
/** Principal variation: space-separated UCI moves. */
|
||||||
|
pv: Option[String] = None,
|
||||||
|
/** Actual depth searched. */
|
||||||
|
depth: Option[Int] = None,
|
||||||
|
/** Text description of the position/move quality. */
|
||||||
|
text: Option[String] = None,
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package de.nowchess.analysis.config
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.Version
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||||
|
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||||
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class JacksonConfig extends ObjectMapperCustomizer:
|
||||||
|
def customize(mapper: ObjectMapper): Unit =
|
||||||
|
mapper.registerModule(new DefaultScalaModule() {
|
||||||
|
override def version(): Version =
|
||||||
|
// scalafix:off DisableSyntax.null
|
||||||
|
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
|
||||||
|
// scalafix:on DisableSyntax.null
|
||||||
|
})
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package de.nowchess.analysis.config
|
||||||
|
|
||||||
|
import de.nowchess.analysis.client.{ChessApiRequestDto, ChessApiResponseDto}
|
||||||
|
import de.nowchess.analysis.dto.{AnalysisRequestDto, AnalysisResponseDto}
|
||||||
|
import de.nowchess.analysis.error.AnalysisErrorDto
|
||||||
|
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||||
|
|
||||||
|
@RegisterForReflection(
|
||||||
|
targets = Array(
|
||||||
|
classOf[AnalysisRequestDto],
|
||||||
|
classOf[AnalysisResponseDto],
|
||||||
|
classOf[ChessApiRequestDto],
|
||||||
|
classOf[ChessApiResponseDto],
|
||||||
|
classOf[AnalysisErrorDto],
|
||||||
|
),
|
||||||
|
registerFullHierarchy = true,
|
||||||
|
)
|
||||||
|
class NativeReflectionConfig
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package de.nowchess.analysis.dto
|
||||||
|
|
||||||
|
/** Request body for the analysis endpoint.
|
||||||
|
*
|
||||||
|
* @param fen
|
||||||
|
* FEN string representing the position to analyse.
|
||||||
|
* @param depth
|
||||||
|
* Engine search depth (1-99). Defaults to 12 when absent.
|
||||||
|
*/
|
||||||
|
case class AnalysisRequestDto(fen: String, depth: Option[Int] = None)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package de.nowchess.analysis.dto
|
||||||
|
|
||||||
|
/** Response from the analysis endpoint.
|
||||||
|
*
|
||||||
|
* @param fen
|
||||||
|
* The analysed FEN.
|
||||||
|
* @param depth
|
||||||
|
* The search depth used.
|
||||||
|
* @param bestMove
|
||||||
|
* Best move in UCI notation (e.g. "e2e4"), or None if not available.
|
||||||
|
* @param evaluation
|
||||||
|
* Centipawn evaluation from white's perspective, or None.
|
||||||
|
* @param mate
|
||||||
|
* Mate-in-N value (positive = white wins, negative = black wins), or None.
|
||||||
|
* @param continuationMoves
|
||||||
|
* Principal variation as list of UCI moves.
|
||||||
|
*/
|
||||||
|
case class AnalysisResponseDto(
|
||||||
|
fen: String,
|
||||||
|
depth: Int,
|
||||||
|
bestMove: Option[String],
|
||||||
|
evaluation: Option[Double],
|
||||||
|
mate: Option[Int],
|
||||||
|
continuationMoves: List[String],
|
||||||
|
)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.nowchess.analysis.error
|
||||||
|
|
||||||
|
case class AnalysisErrorDto(code: String, message: String)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package de.nowchess.analysis.error
|
||||||
|
|
||||||
|
sealed class AnalysisException(val status: Int, val code: String, message: String) extends RuntimeException(message)
|
||||||
|
|
||||||
|
class InvalidFenException(fen: String) extends AnalysisException(400, "INVALID_FEN", s"Invalid FEN string: $fen")
|
||||||
|
|
||||||
|
class AnalysisUpstreamException(cause: Throwable)
|
||||||
|
extends AnalysisException(502, "UPSTREAM_ERROR", s"Chess API unavailable: ${cause.getMessage}")
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package de.nowchess.analysis.error
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.{MediaType, Response}
|
||||||
|
import jakarta.ws.rs.ext.{ExceptionMapper, Provider}
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
class AnalysisExceptionMapper extends ExceptionMapper[AnalysisException]:
|
||||||
|
def toResponse(ex: AnalysisException): Response =
|
||||||
|
Response
|
||||||
|
.status(ex.status)
|
||||||
|
.entity(AnalysisErrorDto(ex.code, ex.getMessage))
|
||||||
|
.`type`(MediaType.APPLICATION_JSON)
|
||||||
|
.build()
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package de.nowchess.analysis.resource
|
||||||
|
|
||||||
|
import de.nowchess.analysis.dto.{AnalysisRequestDto, AnalysisResponseDto}
|
||||||
|
import de.nowchess.analysis.service.AnalysisService
|
||||||
|
import jakarta.annotation.security.PermitAll
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
|
import jakarta.inject.Inject
|
||||||
|
import jakarta.ws.rs.*
|
||||||
|
import jakarta.ws.rs.core.{MediaType, Response}
|
||||||
|
|
||||||
|
import scala.compiletime.uninitialized
|
||||||
|
|
||||||
|
@Path("/api/analysis")
|
||||||
|
@ApplicationScoped
|
||||||
|
class AnalysisResource:
|
||||||
|
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
@Inject
|
||||||
|
var analysisService: AnalysisService = uninitialized
|
||||||
|
// scalafix:on DisableSyntax.var
|
||||||
|
|
||||||
|
/** Analyse a chess position.
|
||||||
|
*
|
||||||
|
* Accepts a FEN string and optional depth, proxies to chess-api.com, and returns structured analysis data.
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/position")
|
||||||
|
@PermitAll
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
|
def analysePosition(body: AnalysisRequestDto): Response =
|
||||||
|
val result = analysisService.analyse(body)
|
||||||
|
Response.ok(result).build()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package de.nowchess.analysis.service
|
||||||
|
|
||||||
|
import de.nowchess.analysis.client.{ChessApiClient, ChessApiRequestDto}
|
||||||
|
import de.nowchess.analysis.dto.{AnalysisRequestDto, AnalysisResponseDto}
|
||||||
|
import de.nowchess.analysis.error.{AnalysisUpstreamException, InvalidFenException}
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
|
import jakarta.inject.Inject
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||||
|
import org.jboss.logging.Logger
|
||||||
|
|
||||||
|
import scala.compiletime.uninitialized
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
class AnalysisService:
|
||||||
|
|
||||||
|
private val log = Logger.getLogger(classOf[AnalysisService])
|
||||||
|
|
||||||
|
private val DefaultDepth = 12
|
||||||
|
private val MinDepth = 1
|
||||||
|
private val MaxDepth = 99
|
||||||
|
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
@Inject
|
||||||
|
@RestClient
|
||||||
|
var chessApiClient: ChessApiClient = uninitialized
|
||||||
|
// scalafix:on DisableSyntax.var
|
||||||
|
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
def analyse(request: AnalysisRequestDto): AnalysisResponseDto =
|
||||||
|
val fen = request.fen.trim
|
||||||
|
if fen.isEmpty then throw InvalidFenException(fen)
|
||||||
|
validateFen(fen)
|
||||||
|
|
||||||
|
val depth = request.depth
|
||||||
|
.map(d => d.max(MinDepth).min(MaxDepth))
|
||||||
|
.getOrElse(DefaultDepth)
|
||||||
|
|
||||||
|
log.debugf("Analysing FEN '%s' at depth %d", fen, depth)
|
||||||
|
|
||||||
|
val apiResponse =
|
||||||
|
try chessApiClient.analyse(ChessApiRequestDto(fen, depth))
|
||||||
|
catch
|
||||||
|
case ex: Exception =>
|
||||||
|
log.warnf(ex, "Chess API call failed for FEN '%s'", fen)
|
||||||
|
throw AnalysisUpstreamException(ex)
|
||||||
|
|
||||||
|
val continuationMoves = apiResponse.pv
|
||||||
|
.map(_.split(" ").toList.filter(_.nonEmpty))
|
||||||
|
.getOrElse(List.empty)
|
||||||
|
|
||||||
|
AnalysisResponseDto(
|
||||||
|
fen = fen,
|
||||||
|
depth = apiResponse.depth.getOrElse(depth),
|
||||||
|
bestMove = apiResponse.move,
|
||||||
|
evaluation = apiResponse.centipawns,
|
||||||
|
mate = apiResponse.mate,
|
||||||
|
continuationMoves = continuationMoves,
|
||||||
|
)
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|
||||||
|
/** Rudimentary FEN structure validation — checks the board part has 8 ranks. */
|
||||||
|
// scalafix:off DisableSyntax.throw
|
||||||
|
private def validateFen(fen: String): Unit =
|
||||||
|
val parts = fen.split(" ")
|
||||||
|
if parts.length < 1 then throw InvalidFenException(fen)
|
||||||
|
val ranks = parts(0).split("/")
|
||||||
|
if ranks.length != 8 then throw InvalidFenException(fen)
|
||||||
|
// scalafix:on DisableSyntax.throw
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
quarkus:
|
||||||
|
rest-client:
|
||||||
|
chess-api:
|
||||||
|
url: http://localhost:9999
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
package de.nowchess.analysis.resource
|
||||||
|
|
||||||
|
import de.nowchess.analysis.dto.{AnalysisRequestDto, AnalysisResponseDto}
|
||||||
|
import de.nowchess.analysis.error.{AnalysisUpstreamException, InvalidFenException}
|
||||||
|
import de.nowchess.analysis.service.AnalysisService
|
||||||
|
import io.quarkus.test.InjectMock
|
||||||
|
import io.quarkus.test.junit.QuarkusTest
|
||||||
|
import io.restassured.RestAssured
|
||||||
|
import io.restassured.http.ContentType
|
||||||
|
import org.hamcrest.Matchers.*
|
||||||
|
import org.junit.jupiter.api.{DisplayName, Test}
|
||||||
|
import org.mockito.ArgumentMatchers.any
|
||||||
|
import org.mockito.Mockito.when
|
||||||
|
|
||||||
|
import scala.compiletime.uninitialized
|
||||||
|
|
||||||
|
// scalafix:off
|
||||||
|
@QuarkusTest
|
||||||
|
@DisplayName("AnalysisResource")
|
||||||
|
class AnalysisResourceTest:
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
var analysisService: AnalysisService = uninitialized
|
||||||
|
|
||||||
|
private val validFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||||
|
|
||||||
|
private def givenJson() = RestAssured.`given`().contentType(ContentType.JSON)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/analysis/position returns 200 with analysis data")
|
||||||
|
def testAnalysePositionOk(): Unit =
|
||||||
|
when(analysisService.analyse(any()))
|
||||||
|
.thenReturn(
|
||||||
|
AnalysisResponseDto(
|
||||||
|
fen = validFen,
|
||||||
|
depth = 12,
|
||||||
|
bestMove = Some("e2e4"),
|
||||||
|
evaluation = Some(0.3),
|
||||||
|
mate = None,
|
||||||
|
continuationMoves = List("e2e4", "e7e5"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
givenJson()
|
||||||
|
.body(s"""{"fen": "$validFen"}""")
|
||||||
|
.when()
|
||||||
|
.post("/api/analysis/position")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("fen", equalTo(validFen))
|
||||||
|
.body("depth", equalTo(12))
|
||||||
|
.body("bestMove", equalTo("e2e4"))
|
||||||
|
.body("evaluation", equalTo(0.3f))
|
||||||
|
.body("continuationMoves", hasItems("e2e4", "e7e5"))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/analysis/position returns 400 for invalid FEN")
|
||||||
|
def testAnalysePositionInvalidFen(): Unit =
|
||||||
|
when(analysisService.analyse(any()))
|
||||||
|
.thenThrow(new InvalidFenException("bad-fen"))
|
||||||
|
|
||||||
|
givenJson()
|
||||||
|
.body("""{"fen": "bad-fen"}""")
|
||||||
|
.when()
|
||||||
|
.post("/api/analysis/position")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(400)
|
||||||
|
.body("code", equalTo("INVALID_FEN"))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/analysis/position returns 502 on upstream failure")
|
||||||
|
def testAnalysePositionUpstreamError(): Unit =
|
||||||
|
when(analysisService.analyse(any()))
|
||||||
|
.thenThrow(new AnalysisUpstreamException(new RuntimeException("timeout")))
|
||||||
|
|
||||||
|
givenJson()
|
||||||
|
.body(s"""{"fen": "$validFen"}""")
|
||||||
|
.when()
|
||||||
|
.post("/api/analysis/position")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(502)
|
||||||
|
.body("code", equalTo("UPSTREAM_ERROR"))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/analysis/position accepts custom depth")
|
||||||
|
def testAnalysePositionCustomDepth(): Unit =
|
||||||
|
when(analysisService.analyse(any()))
|
||||||
|
.thenReturn(
|
||||||
|
AnalysisResponseDto(
|
||||||
|
fen = validFen,
|
||||||
|
depth = 20,
|
||||||
|
bestMove = Some("d2d4"),
|
||||||
|
evaluation = Some(0.15),
|
||||||
|
mate = None,
|
||||||
|
continuationMoves = List.empty,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
givenJson()
|
||||||
|
.body(s"""{"fen": "$validFen", "depth": 20}""")
|
||||||
|
.when()
|
||||||
|
.post("/api/analysis/position")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("depth", equalTo(20))
|
||||||
|
// scalafix:on
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
package de.nowchess.analysis.service
|
||||||
|
|
||||||
|
import de.nowchess.analysis.client.{ChessApiClient, ChessApiRequestDto, ChessApiResponseDto}
|
||||||
|
import de.nowchess.analysis.dto.AnalysisRequestDto
|
||||||
|
import de.nowchess.analysis.error.{AnalysisUpstreamException, InvalidFenException}
|
||||||
|
import io.quarkus.test.InjectMock
|
||||||
|
import io.quarkus.test.junit.QuarkusTest
|
||||||
|
import jakarta.inject.Inject
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.{DisplayName, Test}
|
||||||
|
import org.mockito.ArgumentMatchers.any
|
||||||
|
import org.mockito.Mockito.{verify, when}
|
||||||
|
|
||||||
|
import scala.compiletime.uninitialized
|
||||||
|
|
||||||
|
// scalafix:off
|
||||||
|
@QuarkusTest
|
||||||
|
@DisplayName("AnalysisService")
|
||||||
|
class AnalysisServiceTest:
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
var service: AnalysisService = uninitialized
|
||||||
|
|
||||||
|
@InjectMock
|
||||||
|
@RestClient
|
||||||
|
var chessApiClient: ChessApiClient = uninitialized
|
||||||
|
|
||||||
|
private val validFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("analyse returns response with best move from chess-api.com")
|
||||||
|
def testAnalyseReturnsBestMove(): Unit =
|
||||||
|
when(chessApiClient.analyse(any()))
|
||||||
|
.thenReturn(
|
||||||
|
ChessApiResponseDto(
|
||||||
|
move = Some("e2e4"),
|
||||||
|
centipawns = Some(0.3),
|
||||||
|
mate = None,
|
||||||
|
pv = Some("e2e4 e7e5 g1f3"),
|
||||||
|
depth = Some(12),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = service.analyse(AnalysisRequestDto(validFen, Some(12)))
|
||||||
|
|
||||||
|
assertEquals(validFen, response.fen)
|
||||||
|
assertEquals(12, response.depth)
|
||||||
|
assertEquals(Some("e2e4"), response.bestMove)
|
||||||
|
assertEquals(Some(0.3), response.evaluation)
|
||||||
|
assertEquals(None, response.mate)
|
||||||
|
assertEquals(List("e2e4", "e7e5", "g1f3"), response.continuationMoves)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("analyse uses default depth 12 when not specified")
|
||||||
|
def testAnalyseUsesDefaultDepth(): Unit =
|
||||||
|
when(chessApiClient.analyse(any()))
|
||||||
|
.thenReturn(ChessApiResponseDto(move = Some("d2d4"), depth = Some(12)))
|
||||||
|
|
||||||
|
val response = service.analyse(AnalysisRequestDto(validFen))
|
||||||
|
|
||||||
|
verify(chessApiClient).analyse(ChessApiRequestDto(validFen, 12))
|
||||||
|
assertEquals(12, response.depth)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("analyse clamps depth to [1, 99]")
|
||||||
|
def testAnalyseClampsDepth(): Unit =
|
||||||
|
when(chessApiClient.analyse(any()))
|
||||||
|
.thenReturn(ChessApiResponseDto(move = Some("e2e4"), depth = Some(99)))
|
||||||
|
|
||||||
|
service.analyse(AnalysisRequestDto(validFen, Some(200)))
|
||||||
|
|
||||||
|
verify(chessApiClient).analyse(ChessApiRequestDto(validFen, 99))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("analyse clamps depth minimum to 1")
|
||||||
|
def testAnalyseClampsDepthMin(): Unit =
|
||||||
|
when(chessApiClient.analyse(any()))
|
||||||
|
.thenReturn(ChessApiResponseDto(move = Some("e2e4"), depth = Some(1)))
|
||||||
|
|
||||||
|
service.analyse(AnalysisRequestDto(validFen, Some(0)))
|
||||||
|
|
||||||
|
verify(chessApiClient).analyse(ChessApiRequestDto(validFen, 1))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("analyse handles empty pv gracefully")
|
||||||
|
def testAnalyseEmptyPv(): Unit =
|
||||||
|
when(chessApiClient.analyse(any()))
|
||||||
|
.thenReturn(ChessApiResponseDto(move = Some("e2e4"), pv = None, depth = Some(5)))
|
||||||
|
|
||||||
|
val response = service.analyse(AnalysisRequestDto(validFen, Some(5)))
|
||||||
|
|
||||||
|
assertEquals(List.empty, response.continuationMoves)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("analyse throws InvalidFenException for empty FEN")
|
||||||
|
def testAnalyseThrowsOnEmptyFen(): Unit =
|
||||||
|
assertThrows(
|
||||||
|
classOf[InvalidFenException],
|
||||||
|
() => service.analyse(AnalysisRequestDto("")),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("analyse throws InvalidFenException for malformed FEN")
|
||||||
|
def testAnalyseThrowsOnMalformedFen(): Unit =
|
||||||
|
assertThrows(
|
||||||
|
classOf[InvalidFenException],
|
||||||
|
() => service.analyse(AnalysisRequestDto("not/a/valid/fen")),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("analyse wraps chess-api.com exception in AnalysisUpstreamException")
|
||||||
|
def testAnalyseWrapsUpstreamException(): Unit =
|
||||||
|
when(chessApiClient.analyse(any()))
|
||||||
|
.thenThrow(new RuntimeException("connection refused"))
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
classOf[AnalysisUpstreamException],
|
||||||
|
() => service.analyse(AnalysisRequestDto(validFen)),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("analyse returns mate value from chess-api.com response")
|
||||||
|
def testAnalyseReturnsMate(): Unit =
|
||||||
|
when(chessApiClient.analyse(any()))
|
||||||
|
.thenReturn(
|
||||||
|
ChessApiResponseDto(
|
||||||
|
move = Some("d1h5"),
|
||||||
|
centipawns = None,
|
||||||
|
mate = Some(3),
|
||||||
|
depth = Some(10),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = service.analyse(AnalysisRequestDto(validFen, Some(10)))
|
||||||
|
|
||||||
|
assertEquals(Some(3), response.mate)
|
||||||
|
assertEquals(None, response.evaluation)
|
||||||
|
// scalafix:on
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VERSION=0.1.0
|
||||||
@@ -2037,3 +2037,75 @@
|
|||||||
|
|
||||||
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
||||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
## (2026-06-10)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
|
||||||
|
* add CORS configuration and reorder JWT settings in application.yml ([a49f9be](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a49f9be146f04c14561c305d980846a92f8c12b2))
|
||||||
|
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
|
||||||
|
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||||
|
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
|
||||||
|
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||||
|
* **config:** add GameWritebackEventDto to reflection targets ([87f29a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87f29a720422f538ef70699533500e060337b8ea))
|
||||||
|
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||||
|
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||||
|
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
|
||||||
|
* **core:** publish GameOver event to Redis Streams ([#64](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/64)) ([676e411](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/676e4110c0893917d8bc7f836db6a19c69c5e9a5))
|
||||||
|
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
|
||||||
|
* implement clock expiry scanning and handling for game records ([#53](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/53)) ([8f9eb12](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8f9eb12f663efabe4dc72b94394438652ad0ef02))
|
||||||
|
* implement GameRules with isInCheck, legalMoves, gameStatus ([94a02ff](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/94a02ff6849436d9496c70a0f16c21666dae8e4e))
|
||||||
|
* implement legal castling ([#1](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/1)) ([00d326c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/00d326c1ba67711fbe180f04e1100c3f01dd0254))
|
||||||
|
* implement periodic scaling checks and enhance instance management in AutoScaler ([3f12f69](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f12f695f132b92f634d98df2c037292498b6e86))
|
||||||
|
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
|
||||||
|
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
|
||||||
|
* NCS-11 50-move rule ([#9](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/9)) ([412ed98](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/412ed986a95703a3b282276540153480ceed229d))
|
||||||
|
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
|
||||||
|
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
|
||||||
|
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
|
||||||
|
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
|
||||||
|
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
|
||||||
|
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
|
||||||
|
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
|
||||||
|
* NCS-40 Rework Draw System ([#34](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/34)) ([33e785d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e785d22af87724839b62ae91dfe74a05b398c3))
|
||||||
|
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
|
||||||
|
* NCS-53 changed IO to MicroService for easier scaling ([#37](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/37)) ([b5a2966](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b5a2966adafa9650f0f7d601bdeb8fdd13710327))
|
||||||
|
* NCS-6 Implementing FEN & PGN ([#7](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/7)) ([f28e69d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f28e69dc181416aa2f221fdc4b45c2cda5efbf07))
|
||||||
|
* NCS-78 Add Traceability to the Applications ([#48](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/48)) ([c96a09b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c96a09bb5cee59fc23205bb63baa8b217a7e1b00))
|
||||||
|
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
|
||||||
|
* NCS-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
|
||||||
|
* **redis:** implement game writeback stream processing with error handling and retries ([ae3ef76](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/ae3ef766e8b7596a09e466cd4fb386119f17ca5c))
|
||||||
|
* **reflection:** add native reflection configuration for tournament classes ([e318250](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e31825021c0fca7cbe7d9f85755646114c83cf0c))
|
||||||
|
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
|
||||||
|
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||||
|
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
|
||||||
|
* wire check/checkmate/stalemate into processMove and gameLoop ([5264a22](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5264a225418b885c5e6ea6411b96f85e38837f6c))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add missing kings to gameLoop capture test board ([aedd787](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/aedd787b77203c2af934751dba7b784eaf165032))
|
||||||
|
* **auth:** change InternalAuthFilter to use @Singleton and add HTTP tests for secret validation ([c08d530](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c08d5303eb9e70d36c8eebf6a061ccb71e118fe5))
|
||||||
|
* **auth:** update InternalAuthFilter to use @ApplicationScoped and add index-dependency configuration ([6e0fd95](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6e0fd9523e001756ce7109e639ebb54be4fcdabf))
|
||||||
|
* **core:** add logs to trace subscribeGame call in createGame ([f5614c3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f5614c358255598ba1230e42a56b22934d79183c))
|
||||||
|
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
|
||||||
|
* **heartbeat:** inject ObjectMapper into InstanceHeartbeatService ([#42](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/42)) ([0c98151](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0c981517da1f94cd10ae396e47bde2b35d0b3ba0))
|
||||||
|
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
|
||||||
|
* Lints ([dc224ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dc224abe26acf5361c56956006e1cc51b75b0b7e))
|
||||||
|
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
|
||||||
|
* NCS-85 Database Writeback fails without Logs ([#52](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/52)) ([7323908](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/73239088d985f01aa6b1067ed9097a845e471d4f))
|
||||||
|
* **pgn:** add SAN disambiguation and check/checkmate suffixes [NCS-42] ([#56](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/56)) ([2579539](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2579539084152178f4482ddb7b84b7f1162f10da))
|
||||||
|
* **redis:** add max pool wait time and switch to ReactiveRedisDataSource for heartbeat updates ([33e5017](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e5017f51a998327b180f778f73964cc10c05d3))
|
||||||
|
* **redis:** enhance GameRedisSubscriberManager to use ReactiveRedisDataSource and improve subscription handling ([0eb752d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0eb752d4935377f75aab710b7f4eda4b29098e6a))
|
||||||
|
* **redis:** prevent concurrent Redis heartbeat refreshes using AtomicBoolean ([847b132](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/847b13202cb909d18ca3304c27ebe17ce2312b8e))
|
||||||
|
* **redis:** simplify refreshRedisHeartbeat logic and ensure proper error handling ([1813ea1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1813ea1d2d5d093f7925f87371b5e29820bf1136))
|
||||||
|
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
|
||||||
|
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
|
||||||
|
* resolve 6 coordinator bugs (cache eviction, rebalance race, pod matching, lookup inefficiency) ([5619c82](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5619c8223ad7091706909eda8c907a29d215fd30))
|
||||||
|
* update documentation to reflect new functions in CoordinatorGrpcServer and InstanceRegistry ([f7ce4df](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f7ce4df595cbdc2ef84122781f4851ff140c0f44))
|
||||||
|
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
|
||||||
|
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
||||||
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
|||||||
@@ -144,6 +144,23 @@ tasks.withType(org.gradle.api.tasks.scala.ScalaCompile::class).configureEach {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraalVM CE javac fails type inference for asyncUnaryCall when request/response types are
|
||||||
|
// both different and when the response type appears as a request type elsewhere in the service.
|
||||||
|
// Patching the generated stub to add explicit type witnesses is the minimal targeted fix.
|
||||||
|
tasks.named("quarkusGenerateCode") {
|
||||||
|
doLast {
|
||||||
|
val grpcDir = file("build/classes/java/quarkus-generated-sources/grpc/de/nowchess/core/proto")
|
||||||
|
grpcDir.walkTopDown().filter { it.name.endsWith("Grpc.java") }.forEach { f ->
|
||||||
|
val original = f.readText()
|
||||||
|
val patched = original.replace(
|
||||||
|
"io.grpc.stub.ClientCalls.asyncUnaryCall(getChannel().newCall(getApplyMoveMethod(), getCallOptions()), request, responseObserver);",
|
||||||
|
"io.grpc.stub.ClientCalls.<de.nowchess.core.proto.ProtoMoveRequest, de.nowchess.core.proto.ProtoGameContext>asyncUnaryCall(getChannel().newCall(getApplyMoveMethod(), getCallOptions()), request, responseObserver);"
|
||||||
|
)
|
||||||
|
if (patched != original) f.writeText(patched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named("compileScoverageJava").configure {
|
tasks.named("compileScoverageJava").configure {
|
||||||
dependsOn(tasks.named("quarkusGenerateCode"))
|
dependsOn(tasks.named("quarkusGenerateCode"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=50
|
MINOR=51
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## (2026-06-10)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* NCS-121 pipeline for tournament ([#68](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/68)) ([145f467](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/145f4676483f92bfe6f2d9ca40e2cb4200982e87))
|
||||||
|
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
|
||||||
|
* **reflection:** add GameWritebackEventDto to native reflection configuration ([1aee39c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1aee39c1ad286984501ac4b47da2b72d60b58a6f))
|
||||||
|
* **reflection:** add native reflection configuration for tournament classes ([65bc6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/65bc6a759937543df2d29905688bfa9e68d0c9d4))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **tournament:** replace scala.util.Random singleton with UUID for native image ([a50884a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a50884a11b1de500e74c18fd08d2d102d53cc3e9))
|
||||||
+2
@@ -1,5 +1,6 @@
|
|||||||
package de.nowchess.tournament.config
|
package de.nowchess.tournament.config
|
||||||
|
|
||||||
|
import de.nowchess.api.dto.GameWritebackEventDto
|
||||||
import de.nowchess.tournament.client.{CoreCreateGameRequest, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
|
import de.nowchess.tournament.client.{CoreCreateGameRequest, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
|
||||||
import de.nowchess.tournament.domain.{Tournament, TournamentPairing, TournamentParticipant}
|
import de.nowchess.tournament.domain.{Tournament, TournamentPairing, TournamentParticipant}
|
||||||
import de.nowchess.tournament.dto.*
|
import de.nowchess.tournament.dto.*
|
||||||
@@ -29,6 +30,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
|||||||
classOf[CoreTimeControl],
|
classOf[CoreTimeControl],
|
||||||
classOf[CoreCreateGameRequest],
|
classOf[CoreCreateGameRequest],
|
||||||
classOf[CoreGameResponse],
|
classOf[CoreGameResponse],
|
||||||
|
classOf[GameWritebackEventDto],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
class NativeReflectionConfig
|
class NativeReflectionConfig
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=1
|
MINOR=2
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
@@ -27,4 +27,5 @@ include(
|
|||||||
"modules:store",
|
"modules:store",
|
||||||
"modules:coordinator",
|
"modules:coordinator",
|
||||||
"modules:tournament",
|
"modules:tournament",
|
||||||
|
"modules:analysis",
|
||||||
)
|
)
|
||||||
Reference in New Issue
Block a user