Skip to content

Conversation

@triceo
Copy link
Collaborator

@triceo triceo commented Jan 20, 2026

This was written entirely using SpecKit. Human+AI collaboration went into the specification; the code produced was 99 % done by Claude Sonnet 4.5.

Important pieces:

  • The actual code and docs.
  • spec.md contains the specification of the feature
  • plan.md (and accompanying data model + API) contains the AI's implementation plan
  • constitution.md - I drafted some amendments as I was watching AI produce code that didn't look like "our" code.

I'm asking both @Christopher-Chianelli and @zepfred for a very careful code review. I have two reasons for that:

  • We should all be aware of what is possible with today's tools, what are its good sides and what are its limitations.
  • We need to pay extra attention to code like this, because it was not produced by any of us.

I do not recommend reviewing commit-by-commit. Review the final product, if you wish to keep your sanity.

* This class is an internal detail and must not be exposed to the user.
*/
@NullMarked
static final class DummyConstraintProvider implements ConstraintProvider {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This forces some changes to Quarkus/Spring.
I made these manually, as I didn't trust the agent to figure it out without all the speccing and planning prerequisites.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need a DummyConstraintProvider? MoveAsserter works without one.

@triceo triceo marked this pull request as ready for review January 20, 2026 11:15
@triceo triceo requested a review from TomCools as a code owner January 20, 2026 11:15
Copilot AI review requested due to automatic review settings January 20, 2026 11:15
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds a new MoveRunner API for testing move implementations during development. The feature was implemented using AI assistance (Claude Sonnet 4.5) based on specifications created through human-AI collaboration.

Changes:

  • Added new testing API (MoveRunner and MoveRunContext) for executing moves with automatic undo support
  • Updated Spring Boot and Quarkus auto-configuration to filter out internal test classes
  • Enhanced configuration utilities to support non-public inner static classes
  • Added comprehensive test coverage using the new API with builtin moves
  • Updated documentation with usage examples and best practices

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
MoveRunner.java New API entry point providing fluent interface for move execution with resource management
MoveRunContext.java New execution context for permanent and temporary move execution
InnerScoreDirector.java Added executeTemporarily variant accepting consumer callback
AbstractScoreDirector.java Implemented new executeTemporarily method delegating to move director
ConfigUtils.java Enhanced to support non-public inner static class instantiation
TimefoldSolverAutoConfiguration.java Added UNIQUENESS_PREDICATE to filter internal classes and abstract types
IncludeAbstractClassesEntityScanner.java Updated to use shared UNIQUENESS_PREDICATE
TimefoldProcessor.java Added UNIQUENESS_PREDICATE and renamed logger constant
Moves.java Updated swap methods with proper type parameterization
neighborhoods.adoc Added comprehensive MoveRunner API documentation
*MoveTest.java (6 files) New test classes demonstrating MoveRunner usage with builtin moves
MoveRunnerTest.java Unit tests for MoveRunner API
Specification files Complete feature specification, design, and implementation plan

@sonarqubecloud
Copy link

Copy link
Contributor

@Christopher-Chianelli Christopher-Chianelli left a comment

Choose a reason for hiding this comment

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

This seems to be duplicating MoveAsserter (which also has the benefit of not needing a dummy ConstraintProvider to function). The tests relies on the initial values of entities in generated solutions; I would rather the entities values be set explictly when reading tests.

try (var runner = MoveRunner.build(Solution.class, Entity.class)) {
// Execute multiple moves in sequence
var context1 = runner.using(solution1);
context1.execute(move1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Where are move1/move2 defined? (I know this is for a docs and doesn't need to compile per se, but the other examples define their moves)

Comment on lines +507 to +515
var solution = createTestSolution();
try {
try (var runner = MoveRunner.build(...)) {
runner.using(solution).executeTemporarily(move, assertions);
}
} catch (Exception e) {
// Solution is now in undefined state - discard it
solution = createTestSolution(); // Create fresh instance
}
Copy link
Contributor

Choose a reason for hiding this comment

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

If a test looked like this, I would call it a bad unit test.

try {
return clazz.getDeclaredConstructor().newInstance();
var ctor = clazz.getDeclaredConstructor();
ctor.setAccessible(true); // For inner static non-public classes; public noarg ctors are effectively not.
Copy link
Contributor

Choose a reason for hiding this comment

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

This will fail when Java enforces encapsulation; when is setAccessible needed?

Comment on lines +35 to +39
@SuppressWarnings("unchecked")
public static <Solution_, Entity_, Value_> Move<Solution_> swap(
PlanningVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel, Entity_ leftEntity, Entity_ rightEntity) {
return swap(List.of((PlanningVariableMetaModel<Solution_, Entity_, Object>) variableMetaModel), leftEntity,
rightEntity);
Copy link
Contributor

Choose a reason for hiding this comment

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

The previous implementation was better

Objects.requireNonNull(exceptionHandler, "exceptionHandler");

try {
scoreDirector.executeMove(Objects.requireNonNull(move, "move"));
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we throw an AssertionError when an exception is not thrown? I would only used this method if I am expecting an exception in a test.

var solution = TestdataSolution.generateSolution(2, 2);
assertThatThrownBy(() -> runner.using(solution))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("closed");
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably should be "The MoveRunner cannot be used after it is closed."


[source,java,options="nowrap"]
----
try (var runner = MoveRunner.build(SolutionClass.class, EntityClass.class)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
try (var runner = MoveRunner.build(SolutionClass.class, EntityClass.class)) {
try (var runner = MoveRunner.build(Solution.class, Entity.class)) {

@Test
void testChangeMove() {
// Arrange
var solution = TestdataSolution.generateSolution(2, 2);
Copy link
Contributor

Choose a reason for hiding this comment

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

I would not use Testdata in the docs and would use an example such as Employee Scheduling

----
@Test
void testMoveThenUndo() {
var solution = TestdataSolution.generateSolution(2, 2);
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

----
@Test
void testMoveWithExceptionHandler() {
var solution = createInvalidSolution(); // May cause exceptions
Copy link
Contributor

Choose a reason for hiding this comment

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

That comment is pointless/not useful; I would create a move that uses an out of bound index

}

@Override
public void executeTemporarily(Move<Solution_> move, Consumer<SolutionView<Solution_>> consumer) {
Copy link
Contributor

Choose a reason for hiding this comment

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

First, let's rename the method to executeTemporaryMove for consistency. Additionally, I suggest merging the logic of the new method with the existing one. This way, we can reuse the logic and pass a dummy consumer when it is not necessary.

*/
InnerScore<Score_> executeTemporaryMove(Move<Solution_> move, boolean assertMoveScoreFromScratch);

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

See my previous related comment. We can provide a default implementation for the method without a consumer.

private final InnerScoreDirector<Solution_, ?> scoreDirector;

MoveRunContext(InnerScoreDirector<Solution_, ?> scoreDirector) {
this.scoreDirector = Objects.requireNonNull(scoreDirector, "scoreDirector");
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder how useful it is to check for nullity when using @NullMarked.

* @throws NullPointerException if move or assertions is null
* @throws IllegalStateException if the parent MoveRunner has been closed
*/
public void executeTemporarily(Move<Solution_> move, Consumer<SolutionView<Solution_>> assertions) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's rename it to executeTemporaryMove.

Objects.requireNonNull(solutionClass, "solutionClass"), entityClasses);

// Create a Constraint Streams configuration with a dummy constraint provider
var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig()
Copy link
Contributor

@zepfred zepfred Jan 20, 2026

Choose a reason for hiding this comment

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

I don't like the idea of using a dummy score provider by default. I would prefer to include the score provider as a parameter and move DummyConstraintProvider to a test package. This way, we can reuse the provider across all move test classes without losing cohesion.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could also provide a facility method that uses the dummy constraint provider by default outside of the MoveRunner class and maintains the current logic of the test move classes.

Copy link
Contributor

@zepfred zepfred Jan 20, 2026

Choose a reason for hiding this comment

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

After reviewing all classes, I see how the dummy score provider simplifies testing. Maybe we can rename the provider class to something like InternalMoveRunnerConstraintProvider and keep the current implementation.

@Test
void changeMoveTemporaryMultipleEntities() {
// Arrange
var solution = TestdataSolution.generateSolution(3, 3);
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto.

Copy link
Contributor

@zepfred zepfred left a comment

Choose a reason for hiding this comment

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

The test coverage looks good. While we have small, self-contained tests, I believe we could move some boilerplate code from these test classes to a parent class to reduce duplication.

The AbstractScoreDirector class can be enhanced, and we should ensure that the names of methods executing temporary moves are consistent with the existing ones.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants