Describe the bug
TimefoldModelDescriptorProcessor#processModelAppJsUI collects a model's META-INF/resources into target/timefold/<modelId>/ui/ by walking the tree and calling Files.copy(resourceFile, destinationPath, StandardCopyOption.REPLACE_EXISTING) for every entry.
Files.copy with REPLACE_EXISTING cannot replace a non-empty directory: it first tries to delete the target and throws DirectoryNotEmptyException. So the collection works once, but fails the second time it runs over the same output directory — and it runs once per Quarkus augmentation. Any mvn verify with more than one @QuarkusTest profile re-augments during test discovery, so the whole test run fails as soon as the model's META-INF/resources contains at least one subdirectory.
Flat resource layouts (top-level files only) are unaffected, because a plain file copy with REPLACE_EXISTING is idempotent. That is why existing models (flat app.js + top-level files) never hit this — a model shipping a standard Vite build (index.html + an assets/ subdirectory with hashed files) hits it immediately.
Expected behavior
Collecting the model's UI resources is idempotent: a second augmentation over an already-populated ui/ output succeeds.
Actual behavior
The second augmentation throws, which surfaces as a test-discovery failure before any test runs:
[ERROR] TestEngine with ID 'junit-jupiter' encountered a critical issue during test discovery:
Cause: org.junit.platform.commons.PreconditionViolationException: Could not load class with name: <the model's @QuarkusTest class>
...
Caused by: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
[error]: Build step ai.timefold.solver.service.quarkus.deployment.TimefoldModelDescriptorProcessor#preparePlatformArchiveForModel threw an exception: java.io.UncheckedIOException: Unexpected IO exception while collecting model's UI resources
Caused by: java.nio.file.DirectoryNotEmptyException: .../target/timefold/<modelId>/ui/assets
at java.base/sun.nio.fs.UnixFileSystem.copy(UnixFileSystem.java:1002)
at java.base/java.nio.file.Files.copy(Files.java:1192)
at ai.timefold.solver.service.quarkus.deployment.TimefoldModelDescriptorProcessor.lambda$processModelAppJsUI$0(TimefoldModelDescriptorProcessor.java:1248)
To Reproduce
- In a model project, add a subdirectory with at least one file under
src/main/resources/META-INF/resources/ (for example assets/index-abc123.js) next to index.html/app.js.
- Have at least two
@QuarkusTest classes whose discovery triggers separate augmentations (for example different test profiles).
- Run
mvn clean test.
Every test class after the first augmentation fails to load with the error above; Tests run: 0.
Root cause
In processModelAppJsUI (TimefoldModelDescriptorProcessor.java, line ~1229 on current main) the directory case is already half-handled:
Files.createDirectories(destinationPath.getParent());
if (Files.isDirectory(destinationPath)) {
Files.createDirectories(destinationPath);
}
Files.copy(resourceFile, destinationPath, StandardCopyOption.REPLACE_EXISTING);
The isDirectory guard creates the directory but then falls through to the unconditional Files.copy, which throws for a directory whose target already exists and is non-empty.
Suggested fix
Skip the copy for directory entries — Files.copy on a directory only ever creates an empty directory anyway (the children are copied by their own walk entries), so nothing is lost:
Files.createDirectories(destinationPath.getParent());
if (Files.isDirectory(resourceFile)) {
Files.createDirectories(destinationPath);
return;
}
Files.copy(resourceFile, destinationPath, StandardCopyOption.REPLACE_EXISTING);
Environment
Timefold Solver Version or Git ref: reproduced with timefold-solver-service-parent 2.5.0-rc-1 and 2.5.0; the method is unchanged on current main.
Output of java -version: openjdk version "21.0.11" 2026-04-21 LTS (also reproduced on the CI's Linux JDK 21)
Output of uname -a or ver: Darwin 25.5.0 (arm64); also reproduced on Linux CI runners.
Additional information
First observed in TimefoldAI/timefold-employee-scheduling#1393 (internal), the first model PR that ships a Vite-built visualisation with an assets/ subdirectory — its clean verify check fails on every commit with this error. The same collection logic is duplicated in the internal models SDK repository, so the fix applies there too.
Describe the bug
TimefoldModelDescriptorProcessor#processModelAppJsUIcollects a model'sMETA-INF/resourcesintotarget/timefold/<modelId>/ui/by walking the tree and callingFiles.copy(resourceFile, destinationPath, StandardCopyOption.REPLACE_EXISTING)for every entry.Files.copywithREPLACE_EXISTINGcannot replace a non-empty directory: it first tries to delete the target and throwsDirectoryNotEmptyException. So the collection works once, but fails the second time it runs over the same output directory — and it runs once per Quarkus augmentation. Anymvn verifywith more than one@QuarkusTestprofile re-augments during test discovery, so the whole test run fails as soon as the model'sMETA-INF/resourcescontains at least one subdirectory.Flat resource layouts (top-level files only) are unaffected, because a plain file copy with
REPLACE_EXISTINGis idempotent. That is why existing models (flatapp.js+ top-level files) never hit this — a model shipping a standard Vite build (index.html+ anassets/subdirectory with hashed files) hits it immediately.Expected behavior
Collecting the model's UI resources is idempotent: a second augmentation over an already-populated
ui/output succeeds.Actual behavior
The second augmentation throws, which surfaces as a test-discovery failure before any test runs:
To Reproduce
src/main/resources/META-INF/resources/(for exampleassets/index-abc123.js) next toindex.html/app.js.@QuarkusTestclasses whose discovery triggers separate augmentations (for example different test profiles).mvn clean test.Every test class after the first augmentation fails to load with the error above;
Tests run: 0.Root cause
In
processModelAppJsUI(TimefoldModelDescriptorProcessor.java, line ~1229 on currentmain) the directory case is already half-handled:The
isDirectoryguard creates the directory but then falls through to the unconditionalFiles.copy, which throws for a directory whose target already exists and is non-empty.Suggested fix
Skip the copy for directory entries —
Files.copyon a directory only ever creates an empty directory anyway (the children are copied by their own walk entries), so nothing is lost:Environment
Timefold Solver Version or Git ref: reproduced with
timefold-solver-service-parent2.5.0-rc-1 and 2.5.0; the method is unchanged on currentmain.Output of
java -version: openjdk version "21.0.11" 2026-04-21 LTS (also reproduced on the CI's Linux JDK 21)Output of
uname -aorver: Darwin 25.5.0 (arm64); also reproduced on Linux CI runners.Additional information
First observed in TimefoldAI/timefold-employee-scheduling#1393 (internal), the first model PR that ships a Vite-built visualisation with an
assets/subdirectory — itsclean verifycheck fails on every commit with this error. The same collection logic is duplicated in the internal models SDK repository, so the fix applies there too.