Clear your Kotlin’s code mutation report using Jev

Mutation testing is making a come back and rightfully so. Now that we have agents doing all the testing for us we need to make sure that they’ve written all necessary tests, making our test suite stronger and our project better.

Mutation testing

In a nutshell, mutation testing is a way to check if the tests you’ve written are covering all cases. The process is simple:

  1. You go and change the production code by either removing code (without breaking the compilation) or altering it (for example change the conditions in an if-else statement)
  2. You run the test suite. If nothing fails then your tests were not covering all cases.

Lets see an example:

// production
class AdmissionPolicy {
fun canEnter(age: Int): Boolean = age >= 18
}
// tests
class AdmissionPolicyTest {
@ParameterizedTest(name = "age {0}: admission allowed = {1}")
@CsvSource("17, false", "19, true")
fun `admission is granded for 18 and above`(age: Int, expected: Boolean) {
assertEquals(expected, AdmissionPolicy().canEnter(age))
}
}

The admission test is making sure that the code works as expected. Anyone under 18 is being denied access and anyone over it is granted.

Lets mutate the production code by changing age >= 18 to age > 18 and run the tests again. Turns out that all tests pass despite the change. This means that if the same change is done by a dev there will be nothing stopping it to get to production. The mutation survived!

Fortunately mutation testing exposes these cases and we can patch them before they reach our users. Here the fix is adding one more test entry in the CSV source:

class AdmissionPolicyTest {
@ParameterizedTest(name = "age {0}: admission allowed = {1}")
@CsvSource("17, false", "18, true", "19, true")
fun `admission is granded for 18 and above`(age: Int, expected: Boolean) {
assertEquals(expected, AdmissionPolicy().canEnter(age))
}
}

PIT

Doing that by hand is tedious so the community has written libraries that mutate the code on their own. In the JVM world the preferred library is PIT. PIT supports lots of mutations and what makes it special is that it does not mutate your code directly. It changes the produced byte code which makes the entire procedure faster.

Unfortunately when it comes to Kotlin, the library’s advantage is also its disadvantage. In order to be able to run in JVM, Kotlin’s compiler produces Java compatible bytecode which means it adds more code to the original file:

// kotlin
fun greet(name: String): String {
return "Hello, $name"
}
// java compatible translation
public final class ExampleKt {
@NotNull
public static final String greet(@NotNull String name) {
return "Hello, " + name;
}
}

This means that PIT might mutate code that was not there on the first place and produce a report with a lot of noise.

Clearing the report

At this point the only thing we can do is to clear the report. This means that for every entry in it we have to:

  1. Check that it is one of the survived mutations
  2. Make sure that the change is on a piece of code that exists on the source file

If both are true then we keep the entry and proceed with a fix. If we are dealing with a couple of mutations then this can be done manually. In any other case we have to automate things:

This is what I’ve ended up with:

Step #1: Keep survived mutations

PIT adds all cases in the same report so the first step was to create a script that will take the initial report as input and produce a second report that contains only the survived mutations.

Step #2: Clean data

To be honest this step is optional. Every entry in the report contains lots of data which makes things difficult to parse manually and reason with. This step uses a script to keep only the information we find important and put them in a single line. Easier to read and comprehend.

Step #3: Remove invalid mutations – inserted methods

Every entry has a pair of “mutatedMethod” and “sourceFile” values. This step uses a script that extracts these values and uses them to make an actual search of the method in the source file. If there are no findings then the mutation is not valid (not on our original code) and gets ignored.

An example of such a value is: < mutatedMethod >invokeSuspend</ mutatedMethod > where invokeSuspend is something that the compiler adds whenever we have coroutines in our Kotlin code.

Step #4: Prepare data for removing rest of invalid mutations

Apart from inserted methods we might have insertions of code in existing methods. This makes the cleanup a bit more difficult since we must do an actual check of both our source code and the change the library did.

For example one of the mutations is described by the tool as:

removed call to kotlin/ResultKt::throwOnFailure

In order to see if this is a valid mutation we need to make sure that the original code had a call to the throwOnFailure in the first place. If not then this is invalid.

Since this cannot be done manually we are going to use Jev and to make things easier this step prepares the state that we are going to send to it. So for the aforementioned mutation the preparation is to create a file with the following:

Code:
override suspend fun add(transaction: Transaction) {
mutex.withLock {
transactions.add(transaction)
}
}
Change:
removed call to kotlin/ResultKt::throwOnFailure
view raw jev-state.txt hosted with ❤ by GitHub

Everything is being handled by an agent that is being prompted to find the entry’s method, extract it from the rest of the code and put it in a file along side the mutation’s description.

Step #5: Jev checks mutation’s validity

This step uses a script that for every created file it makes a call to Jev and ask it to confirm if the mutation is valid. In the call it passes the file’s content as the state and asks a Noul question:

Given the Kotlin code before any change, is it possible for the mentioned change to be applied in the code?

Jev answers with a value between 0 and 1. If the value is smaller than 0.5 then the file gets ignored. Any other case moves the file to the next step.

Step #6: Kill mutations!

Here we pass our findings to an agent and prompt it to fix the mutation’s testing results:

Fix the valid surviving PIT mutations by adding or strengthening regression tests that kill them. Do not apply the mutations to production code or change production semantics…

Engineer yourself out of writing and reading code

I’ve been using coding agents for a year and for the better part of it I haven’t written code by hand. I’ve seen a small bump in my speed and productivity but turns out I was using it wrong.

Delegating only the implementation

The way I was using it was close to the way I’ve been working all these years:

  1. I start thinking about the action plan and write it to the agent in every detail
  2. I ask the agent to execute it
  3. I review its job just to make sure that it followed the steps exactly
  4. Finally I open a PR and ask from a peer to review the code as well

Step (2) is the difference with before and it is the single point that gave me that bump. It is obvious that I wasn’t leveraging the power of agents enough and that puzzled me as to why? Why wasn’t I able to trust the agent more? Turns out it is because I remain a “Buildgineer”!

Buildgineer

I heard the term in this presentation where the speaker was explaining that in all other engineering fields, the engineering of a project and the building of it are obviously two separate things. The example that he shares is the one of the golden gate bridge. Many people helped into building it but none of them was the engineer. The engineer was at his office, 2000 miles away. His job was to make the design, to create the plans that will prove the bridge will hold, to set the processes for maintenance etc.

Software engineers on the other hand have been mixing the two aspects from the beginning. It is hard to separate them and to trust that an agent will do the building as we would.

Trusting the agent

So the goal was to reduce my evolvement while trusting that the agent will deliver what we want by maintaining the quality and conventions we have. In other words the previous steps become:

  1. I think about the action plan but write nothing about the implementation
  2. I ask the agent to execute it and to provide a proof of its work
  3. I ask another agent to review the code based on our rules

For step (1) it is important to have a variety of skills in the project. Skills that will guide the agent to create something as you would, skills that will help the agent reason about an ambiguity just like you would, skills that will allow the agent to find the proper tools just like you would! Skills, skills, skills!
This allows the engineer to provide a simpler plan that describes what needs to be done and contains trigger words for skill discovery.

For step (2) it is important to have a rule in your claude/agents md file that forces the agent to create the proof. Depending on the change the proof will vary from a simple test to a video showcasing the feature.

For step (3) it is important to have a separate agent to do the reviewing by forcing a set of rules that help the project maintain its standards.
Tip: one thing that worked great into creating this set was to point the agent to the on-boarding guide we have. A guide that explains through examples the architecture, our conventions, the way we approach a few things. That led into a first draft that was 90% ready for use.

-50% from start to finish

I admit this -50% might not be that accurate but I feel that the truth is close to it. We adopted this way of working at the android team I work at and the numbers were quite promising.

In particular, this past week the PRs that opened and closed in the same week were 18 when the previous two weeks they were 13 and 15 respectively. Even better, the median time-to-merge was reduced 93%, from 30h to 1.88h.

I know that it is too early to draw conclusions but it is a good start especially if we take under consideration that this week the majority of the devs that adopted the POW-workflow were mostly the seniors in out team.

Moving forward

So what will we do from now on? Are we simple prompters? I say no.

We are engineers and we have to make sure that:

  • we provide a solid plan
  • we have laid out the rails on which the agents will work
  • we created the proper feedback loops that keep the agent working as we want

That never ends, it needs constant evolvement and maintenance.

Use AI _in_ a tool not _as_ a tool

Or to be more exact, use a coding agent in a tool. This is my new starting point every time I begin to think of a tool I want to implement.

Being part of the pipeline

A couple of weeks ago I thought of creating an OpenCode agent that will check my uncommitted changes, construct a commit message and finally make the actual commit.

The first version was ok but I wanted to make a few changes. First I wanted the agent to follow certain guidelines, so I edited the agent file. Then I wanted to change the way it constructed the git command, so I edited the agent file. Finally I wanted to support the usage of GitButler, so I edited the agent file.

At that point I realized that I was violating the single responsibility principle big time. No matter the change I’ve kept editing the same component. And it hit me, fascinated by all the things I was able to do with a coding agent I failed to apply good engineering practices when it comes to the construction of tools.

Small coherent components that do one thing and do it well

When we write code we tend to break it into small modules, classes, functions that have one responsibility and provide a lean API. This way we can reuse components, combine them in different ways and replace them easily.

No need to do the opposite when it comes to tooling. The terminal has led the way by having small tools that do one thing (ls, grep, cat etc) and can be combined, by using pipes, into an entire workflow. My tools need to embrace that as well.

ai-commit.sh

So I broke my agent’s workflow into distinct components:

  1. Create a single prompt from collecting all changes. This can be done by a bash script.
  2. Feed the prompt to an agent that is configured to create a title and a message. This can be done by OpenCode with a custom agent.
  3. Get the agent’s output and use it to make a commit using the appropriate tool. This can be done by a bash script.

I created the two scripts and also wrote one more that ties everything together: ai-commit.sh

In case you are wondering why Hemingway (or Hemi for the friends) is in the picture, it’s just the name I gave to what was left from the original agent: hemi.md

Pinky and Brain v2

Three months I wrote about Pinky and Brain, my agent/subagent duo that was helping me plan and execute a task while being in the loop. I was using it quite often but whenever I did I also saw the number of my available copilot requests decreasing fast! So I started avoiding it and preferred writing code manually like a caveman.

My setup was a testimony of using coding agent as a tool. It was doing everything. Apart from planning, that should be done by it, it was also looping through tasks, delegating work, making commits, asking for the user’s approval to continue looping. Many of these operations where new requests (x3 because of Opus).

So I sat down and broke that too:

  1. Planning is still being make by an agent (Brain). Only this time it is not tied to a model and it is very restricted. It can only save the created tasks to beads which will be loaded as a skill.
  2. Execution is still being done by an agent (Pinky). Only this time it is even more simpler. It is asked to just follow instructions nothing else.
  3. Everything else is part of a bash script that (a) uses bd to get the next task, (b) provides its description as a prompt to Pinky, (c) closes the task when Pinky returns, (d) uses ai-commit.sh to create a commit, (e) loop again.

I’ve been using it for a couple of weeks now and I’m sure I have a better consumption of requests.

Pinky and the Brain: my agent/subagent duo

OpenCode‘s agents configuration and Beads. A match made in heaven.

My flow until now

Working on a task starts with the Plan agent. I provide an initial prompt of what I want to achieve and the agent responds with a plan. If the plan needs adjustments I ask the agent to update it. When I’m satisfied with the final outcome I move to the execution of the plan. This will happen in one of two ways:

  1. If the context window is still small I simply change agents1 (move to Build) and ask it to proceed with the execution.
  2. If the context window is already big I ask the agent to save the plan in a markdown file, start a new session and ask the Build agent to read the file and execute the plan.

This flow works but there are a few drawbacks that bother me:

  • There is no human in the loop. I end up reviewing all changes at the end of the execution.
  • Even if I start a new session, depending on the size of the plan, the context window might get big causing the agent to misbehave. Especially in changes that must be repeated.
  • I usually use Opus for planning and Haiku for execution. There are times though that I forget to make the change ending up using Opus for everything. Opus is good but is also expensive!
  • You can’t easily pause the flow and continue from where you stopped.

My new flow

My new flow is based on one agent, one subagent and a database. In particular:

  1. Like before I start with an agent that will help me build a detailed plan that consists from a number of tasks.
  2. When I’m happy with the plan I ask the agent to use Beads and save each task under an epic.
  3. Then I ask the agent to start the execution loop.

Execution loop

  1. The agent uses Beads to figure out which task must be executed. It changes its status to in_progress and asks the subagent to execute it.
  2. The subagent reads the task, makes the necessary changes and informs the agent that it finished.
  3. The agent asks me to review the changes and approve them or not.
  4. If I approve the changes, the agent commits them, close the task and move to the next one.
  5. If I request changes, the agent asks the subagent to make them. At this point we move to step 2 again. We remain at this inner loop until I give my approval.

Pinky and the Brain2

If you didn’t make the connection yet, Brain is the name I gave to the agent (type primary) and Pinky is the subagent.

I did not created them on my own. I asked OpenCode to help me by describing the flow I wanted. OpenCode read its own docs, asked me a couple of clarifying questions and came up with these:
Brain: https://gist.github.com/le0nidas/aae1c9f1b35110a00b7157b6c2437444
Pinky: https://gist.github.com/le0nidas/b8a3a89131a639e39e42f7aaf794cf33

Benefits

  • I am finally in the loop. I review fewer changes at a time and sooner!
  • Using subagents for each task keeps both the agent’s and the subagent’s context window smaller and cleaner ending up in fewer, to none, misbehaviors.
  • Brain is tied with Opus and Pinky with Haiku. No need to remember to change anything!
  • The best of all, with Beads I can pause and resume whenever I want. The agent knows where to start from!

PS: if you are part of team and don’t want to pollute the codebase with various configurations, you can (a) init beads in stealth mode and (b) exclude .opencode folder from git

  1. according to the docs, all primary agents share the main conversation hence share the same context window ↩︎
  2. https://www.imdb.com/title/tt0112123/ ↩︎

Working with checkpoints

There are times that my workflow involves a lot of small and consecutive commits. Commits that their message does not really matter since I will squash them into one that describes my work.

An example is when TDDing a certain functionality. In that case I usually write the test, make it pass and finally make a commit.

Why am I doing that? I see it like small checkpoints. I conclude a part of the functionality so I save it. This helps in restoring my code back to last point that I was happy with its state.

One commit

When I first started working this way I was making a distinct checkpoint for each part. Soon enough I realized that these commits didn’t provide any value. I was making them quick for just saving the code and their message was something like save or checkpoint or t.

So instead of doing this and having to squash lots of commits I started using amend. One commit for the first checkpoint and amend for the rest of them. This way, when I’m finished, I rename the HEAD of the branch to something descriptive and move on.

Lots of steps for one commit

I write my code using an IDE (Android Studio or Intellij IDEA) but when it comes to git I move to a terminal.

This means that for committing I have to (1) move to the terminal, (2) make the proper commit/amend and (3) move back to the IDE. Three steps for one save!

Alt + P

So I decided to fix it.

First the bash script that makes the commit:

#!/bin/bash
CHECKPOINT="checkpoint"
git add .
if [[ "$(git log --format=%B -n 1 HEAD | cat)" == "$CHECKPOINT" ]]; then
git commit --amend --no-edit
else
git commit -m"$CHECKPOINT"
fi
view raw checkpoint.sh hosted with ❤ by GitHub

A simple script that either makes a commit with the message checkpoint or amends the staged changes.

Second the import of this script to the IDE:

The Intellij platform provides a functionality called External Tools:
– Go to Settings -> Tools -> External Tools and click on the add button.
– Set the path of your script where it says Program .
– Disable the Open console for tool output if you don’t want to see the result of your script.

At this point you can either use checkpoint as an action (double shift, type checkpoint) or you can go a step further and create a keyboard shortcut:

Go to Settings -> Keymap -> External Tools -> Right click on the script -> Add Keyboard shortcut.

So now every time I want to create a checkpoint I simple press `Alt + P` and continue working without moving from one program to another!

This is how I use Todoist

Disclaimer: this is not a paid post. I wrote it because I like the app and find it helpful. I also want to see, in a year, what has changed in the way I use it.

I always have a notebook next to my keyboard. I use it when trying to solve a bug or put in place a new feature. There was also a time that I used it to plan my day or keep notes for things that I wanted to ask or communicate. That didn’t last long since it wasn’t scaling!

That’s when I decided to move to a digital solution and search for the best to-do app. To be honest I can’t remember how I found out about Todoist. What I do remember though was that I did not check any other apps. Both its amazing human language parser and its shortcuts got me hooked immediately!

My usage

A little context. I use Todoist for over a year and only for work. That means that I don’t take advantage of their projects support. Every task gets added to the #Inbox which is my main driver. Throughout this year I’ve tried many setups and ways to incorporate my needs into the app. Here is how I use it:

Plan my day by setting the tasks that need completion

Every morning I see what needs to be done and create a task for it.

That does not mean that I open the company’s project management tool and copy whatever is assigned to me. I add only what cannot be tracked by the management tool. For instance, if a PR of mine got approved I add a task to merge my work in the main branch.

Also, if a meeting ends up with a couple of actionable items for me, I make sure to add them to Todoist. For example, talk to product about blah blah, comment on this thread, read that article, etc.

Another great source of action items is email. I go by them one by one and if something requires my attention I make a task for it.

Help me build habits

I try to cut down any distractions and one of them is looking at my emails every once in a while. What seems to work for me is to check them in the morning and create, if needed, tasks from them.

To force me in making it a habit I created a task that reminds me every weekday at 8:55 am to check my emails. This is 5 minutes earlier than when I start working so it gets registered, in my mind, as the first thing to do.

To show you the power of Todoist, for creating this task you need to write:

Check emails every weekday at 8:55

It will know what to do:

Reminders

Having a recurring task with a reminder is a good way to document things that do not belong anywhere else.

For example, every two weeks, on a Monday, I need to archive a column in our team’s board and create a new one.

Again, you can write it down

Archive column, create new every two weeks starting mon

and Todoist will understand it:

Write topics, questions, thoughts

Not everything is a task that needs completion. There will be topics and questions that must be communicated in a recurring meeting.

This is where I use labels for each meeting type and a task, with no date, for the topic/question.

This way, every time I am in one of these meetings, I open the label and have a list of what I wanted to discuss.

A task with no date and no label is also my way to write down my thoughts/ideas about the project. A possible refactoring, research for a new tool. Things that I need to get off my head but without setting a deadline.

Filters

I couldn’t close this post without mentioning filters. A feature that took me a while to use but can’t live without it anymore.

Better show you what I mean:

So, this is a filter I run every morning to see:

  • today’s high-priority (P1) tasks or
  • what needs discussion in the team’s stand-up

Another example is

that I use to resurface the thoughts and ideas that I mentioned before.