inkspell
writing topics feed
MODE
THEME
← writing

The Code I Didn't Know How to Write

Twelve years ago a strict red-green-refactor ritual carried me to a solution I didn't know how to design. The trick wasn't thinking harder. It was thinking on schedule.

July 24, 2026 · 8 min read
ai tdd testing

The algorithm deserved a better programmer. Test-driven development made do by rationing the one it had.

In September 2014 I sat down with an algorithm exercise: given an array of integers, find the slice — any run of consecutive elements — whose sum lands closest to zero. There was a performance bound that ruled out anything lazy: N·log(N), if that means something to you; “fast even on huge arrays” if it doesn’t. I had never solved this kind of problem before.

And I made it harder with one rule: perform the ritual properly. Test-driven development, by the book. Write a failing test (that’s the “red”). Write the least code that makes it pass (the “green”). Refactor. Repeat. No sketching the algorithm up front, no building ahead of the tests, no fixing anything — even things I could already see were broken — until a test asked for it.

I want to be honest about how that rule felt: like pressure. Minutes in, I already knew things about the code that would need fixing sooner or later. The ritual kept saying: not now. Your job right now is one failing test.

By the end, I had a solution inside the required bounds. That isn’t the strange part. The strange part was the feeling that came with the result. The code was terse — so terse that reading it didn’t tell me how it worked. But it did work, and the tests kept saying so. Working code beyond my understanding, sitting in my own repository: it felt less like something I had written and more like something I had been handed. I didn’t know it at the time, but that feeling is what good engineering feels like.

I never solved the whole problem

The first test was almost an insult:

@Test
public void testOneElement() {
    assertThat(solution(new int[] {1}), is(1));
}

One element. The slice is the element. And the first implementation was exactly as heroic as the test demanded — return a[0]; — not an abs() in sight, because no test had asked for one yet.

Then two elements. Then three, where mixed signs start to bite, and where it turned out I had misread the requirements. The commit messages from that evening keep me honest. One reads, “Maybe you don’t get the requirements right the first time, but by testing you are very aware of what you are implementing.” Two minutes later: “More tests failing for no reason at all. Maybe one good reason would be using Math.max() instead of Math.min().”

About an hour of small honest steps later I had a general solution: a brute force — try every slice, keep the best. Correct on everything I threw at it, and nowhere near the required speed. The next commit says, “time to put our thinking caps on :)”.

Here is what did not happen next: I did not drop the ritual because the real problem had finally arrived. The commit that follows asks the only question the ritual permits, and gives the only answer it accepts: “So what do we do next? We write a test of course.” So I wrote one — a crude timing test: run the solution at two sizes, complain if the time grew too fast. In its final form it looks like this:

@Test
public void testComplexityIsCloseToN() {
    final int BASE = 5000000;
    final int SIZE_MULTIPLIER = 4;
    int[] low = new int[BASE];
    int[] high = new int[BASE*SIZE_MULTIPLIER];

    long lowTime = timeSolution(low);
    long highTime = timeSolution(high);
    long timeMultiplier = round((double) highTime / lowTime);
    assertThat("The complexity should be close to N (which is N^1) ~ 1",
                    approximateExponent(timeMultiplier, SIZE_MULTIPLIER), is(1L));
}

Wall-clock time and a rounded exponent — benchmarking purists may want to sit down. But it failed while the code was too slow and passed once it was not, and that was all the ritual needed. Performance stopped being an opinion and became a failing test.

Three questions, never all at once

The whole discipline fits in three questions. Red: what should work next? Green: what is the least code that makes it work? Refactor: how do I make this better without changing what it does? At any moment I was allowed to hold exactly one.

There is an obvious objection, and I half-believe it to this day: tiny steps cannot escape a local maximum — polishing a dead end only gets you a shinier dead end. A brute force built of small honest steps is still a brute force, and no amount of refusing-to-think turns it into a clever algorithm. True. It is also not what happened here, because the rule was never “don’t think.” It was “don’t think yet.”

The two real ideas in the final solution — a caching trick, then an ordered set — both arrived on demand, summoned by that failing performance test, and both were validated against everything I had written down so far.

The next morning produced my favorite pair of commits, seven minutes apart. 08:30: “So I think I am done. Let’s run the test and see. Oh my..” 08:37: “Ok. Forgot my abs(). All fixed running all tests and all is green! This is unexpected!”

So, not less thinking. Less thinking per step, more thinking on demand, and never all the thinking at once.

A passing test is a thought you get to put down

Why write tests at all? The answer I keep arriving at: to be able to refactor without fear. But underneath the fearlessness sits something more basic. Every passing test is one less concern I have to keep in my head.

Refactoring narrowed my attention to one question: is this structure better? The suite handled the cases I had already covered.

That is what the ritual had been rationing all along. Not effort — attention. Each phase loads a small set of concerns, and, just as important, unloads the rest onto something that holds them more reliably than I do.

Why this sounds like an AI problem

You may recognize the shape of this from somewhere else. A person can hold only so much in mind at once; a language model can only make good use of so much context. These are not the same mechanism, and I won’t pretend they are. But they fail the same way: pile everything that might be relevant onto the desk, and the answer to the question that is actually relevant gets worse.

The fix I stumbled into in 2014 looks, to me, like the general one. Write what you know down once, in a durable place — tests, notes, a specification. Then, for each decision, bring only the slice that decision needs onto the desk. Red pulls one behavior from the pile. Green pulls one failing test. Refactor pulls a design question and leaves correctness to the suite, which is exactly where it was written down.

Which is why the lesson here is not “make agents do TDD.” It sits one level higher: a good process breaks a problem too large to reason about all at once into smaller problems that can be solved one at a time.

In 2014, the one solving those problems was me on a September evening, out of my depth. Increasingly, that role can belong to an agent with a bounded context window. In either case, the job is not to foresee the finished design. It is to solve the problem at hand with enough context, while the tests protect what has already been implemented.

Your code is exactly as good as your tests

The final commit reads, “And we are finished refactoring.. Shiny, Shiny!” — the sound of a man admiring code he doesn’t entirely believe he wrote.

That is where I left the repository: finished, refactored, and green.

While I was preparing this article, an AI agent doing the background research did something no 2014 version of me ever did. It generated twenty thousand random arrays and compared my solution’s answers with those from a separate brute-force solver. If you’ve heard of property-based testing, this is that, done informally.

The verdict: the code I could not explain disagrees with the truth on roughly a third of random inputs. Not an exotic corner case. A third. The fix, once you see it, is a few lines. I have not applied it — the repository is public, commits and suite and bug all still in residence, if you would like to meet them.

The bug does not ruin the story. It completes it. The suite did exactly what I built it to do: it protected every behavior I thought to write down through every refactor. It never promised more. A green bar is not a certificate of correctness. It is the list of things you are allowed to forget. Here is that list, in its entirety:

assertThat(solution(1), is(1));
assertThat(solution(-1), is(1));
assertThat(solution(1, 2), is(1));
assertThat(solution(-1, 2), is(1));
assertThat(solution(-1, 2, 3), is(1));
assertThat(solution(-1, 1, -1), is(0));
assertThat(solution(5, 2, -1), is(1));
assertThat(solution(1, 2, -1, 4), is(1));
assertThat(solution(5, 2, -1, -1), is(0));
assertThat(solution(2, -4, 6, -3, 9), is(1));

Ten lines. Everything not on those ten lines, you are still carrying, whether you know it or not. My tests were a small specification of the problem, and the code rose precisely to its level — and stopped there.

So the ritual holds, and so does its fine print:

Your code is exactly as good as your tests — and your tests are exactly as good as the questions you thought to ask.

So how do I ask better questions than those ten examples? By specifying properties instead of guessing more inputs. That is property-based testing—and, maybe, the next article.

← Back to all writing
Last login: Fri, Jul 24, 00:00:00 on ttys001
user@inkspell:~/blog read refusing-to-think.md
FILE: refusing-to-think.md DATE: 2026-07-24 WORDS: 1599 READ: ~8m
TAGS: [ai] [tdd] [testing]

The Code I Didn't Know How to Write

// Twelve years ago a strict red-green-refactor ritual carried me to a solution I didn't know how to design. The trick wasn't thinking harder. It was thinking on schedule.

» BEGIN OUTPUT
cat <<'OPENING'

The algorithm deserved a better programmer. Test-driven development made do by rationing the one it had.

In September 2014 I sat down with an algorithm exercise: given an array of integers, find the slice — any run of consecutive elements — whose sum lands closest to zero. There was a performance bound that ruled out anything lazy: N·log(N), if that means something to you; “fast even on huge arrays” if it doesn’t. I had never solved this kind of problem before.

And I made it harder with one rule: perform the ritual properly. Test-driven development, by the book. Write a failing test (that’s the “red”). Write the least code that makes it pass (the “green”). Refactor. Repeat. No sketching the algorithm up front, no building ahead of the tests, no fixing anything — even things I could already see were broken — until a test asked for it.

I want to be honest about how that rule felt: like pressure. Minutes in, I already knew things about the code that would need fixing sooner or later. The ritual kept saying: not now. Your job right now is one failing test.

By the end, I had a solution inside the required bounds. That isn’t the strange part. The strange part was the feeling that came with the result. The code was terse — so terse that reading it didn’t tell me how it worked. But it did work, and the tests kept saying so. Working code beyond my understanding, sitting in my own repository: it felt less like something I had written and more like something I had been handed. I didn’t know it at the time, but that feeling is what good engineering feels like.

I never solved the whole problem

The first test was almost an insult:

@Test
public void testOneElement() {
    assertThat(solution(new int[] {1}), is(1));
}

One element. The slice is the element. And the first implementation was exactly as heroic as the test demanded — return a[0]; — not an abs() in sight, because no test had asked for one yet.

Then two elements. Then three, where mixed signs start to bite, and where it turned out I had misread the requirements. The commit messages from that evening keep me honest. One reads, “Maybe you don’t get the requirements right the first time, but by testing you are very aware of what you are implementing.” Two minutes later: “More tests failing for no reason at all. Maybe one good reason would be using Math.max() instead of Math.min().”

About an hour of small honest steps later I had a general solution: a brute force — try every slice, keep the best. Correct on everything I threw at it, and nowhere near the required speed. The next commit says, “time to put our thinking caps on :)”.

Here is what did not happen next: I did not drop the ritual because the real problem had finally arrived. The commit that follows asks the only question the ritual permits, and gives the only answer it accepts: “So what do we do next? We write a test of course.” So I wrote one — a crude timing test: run the solution at two sizes, complain if the time grew too fast. In its final form it looks like this:

@Test
public void testComplexityIsCloseToN() {
    final int BASE = 5000000;
    final int SIZE_MULTIPLIER = 4;
    int[] low = new int[BASE];
    int[] high = new int[BASE*SIZE_MULTIPLIER];

    long lowTime = timeSolution(low);
    long highTime = timeSolution(high);
    long timeMultiplier = round((double) highTime / lowTime);
    assertThat("The complexity should be close to N (which is N^1) ~ 1",
                    approximateExponent(timeMultiplier, SIZE_MULTIPLIER), is(1L));
}

Wall-clock time and a rounded exponent — benchmarking purists may want to sit down. But it failed while the code was too slow and passed once it was not, and that was all the ritual needed. Performance stopped being an opinion and became a failing test.

Three questions, never all at once

The whole discipline fits in three questions. Red: what should work next? Green: what is the least code that makes it work? Refactor: how do I make this better without changing what it does? At any moment I was allowed to hold exactly one.

There is an obvious objection, and I half-believe it to this day: tiny steps cannot escape a local maximum — polishing a dead end only gets you a shinier dead end. A brute force built of small honest steps is still a brute force, and no amount of refusing-to-think turns it into a clever algorithm. True. It is also not what happened here, because the rule was never “don’t think.” It was “don’t think yet.”

The two real ideas in the final solution — a caching trick, then an ordered set — both arrived on demand, summoned by that failing performance test, and both were validated against everything I had written down so far.

The next morning produced my favorite pair of commits, seven minutes apart. 08:30: “So I think I am done. Let’s run the test and see. Oh my..” 08:37: “Ok. Forgot my abs(). All fixed running all tests and all is green! This is unexpected!”

So, not less thinking. Less thinking per step, more thinking on demand, and never all the thinking at once.

A passing test is a thought you get to put down

Why write tests at all? The answer I keep arriving at: to be able to refactor without fear. But underneath the fearlessness sits something more basic. Every passing test is one less concern I have to keep in my head.

Refactoring narrowed my attention to one question: is this structure better? The suite handled the cases I had already covered.

That is what the ritual had been rationing all along. Not effort — attention. Each phase loads a small set of concerns, and, just as important, unloads the rest onto something that holds them more reliably than I do.

Why this sounds like an AI problem

You may recognize the shape of this from somewhere else. A person can hold only so much in mind at once; a language model can only make good use of so much context. These are not the same mechanism, and I won’t pretend they are. But they fail the same way: pile everything that might be relevant onto the desk, and the answer to the question that is actually relevant gets worse.

The fix I stumbled into in 2014 looks, to me, like the general one. Write what you know down once, in a durable place — tests, notes, a specification. Then, for each decision, bring only the slice that decision needs onto the desk. Red pulls one behavior from the pile. Green pulls one failing test. Refactor pulls a design question and leaves correctness to the suite, which is exactly where it was written down.

Which is why the lesson here is not “make agents do TDD.” It sits one level higher: a good process breaks a problem too large to reason about all at once into smaller problems that can be solved one at a time.

In 2014, the one solving those problems was me on a September evening, out of my depth. Increasingly, that role can belong to an agent with a bounded context window. In either case, the job is not to foresee the finished design. It is to solve the problem at hand with enough context, while the tests protect what has already been implemented.

Your code is exactly as good as your tests

The final commit reads, “And we are finished refactoring.. Shiny, Shiny!” — the sound of a man admiring code he doesn’t entirely believe he wrote.

That is where I left the repository: finished, refactored, and green.

While I was preparing this article, an AI agent doing the background research did something no 2014 version of me ever did. It generated twenty thousand random arrays and compared my solution’s answers with those from a separate brute-force solver. If you’ve heard of property-based testing, this is that, done informally.

The verdict: the code I could not explain disagrees with the truth on roughly a third of random inputs. Not an exotic corner case. A third. The fix, once you see it, is a few lines. I have not applied it — the repository is public, commits and suite and bug all still in residence, if you would like to meet them.

The bug does not ruin the story. It completes it. The suite did exactly what I built it to do: it protected every behavior I thought to write down through every refactor. It never promised more. A green bar is not a certificate of correctness. It is the list of things you are allowed to forget. Here is that list, in its entirety:

assertThat(solution(1), is(1));
assertThat(solution(-1), is(1));
assertThat(solution(1, 2), is(1));
assertThat(solution(-1, 2), is(1));
assertThat(solution(-1, 2, 3), is(1));
assertThat(solution(-1, 1, -1), is(0));
assertThat(solution(5, 2, -1), is(1));
assertThat(solution(1, 2, -1, 4), is(1));
assertThat(solution(5, 2, -1, -1), is(0));
assertThat(solution(2, -4, 6, -3, 9), is(1));

Ten lines. Everything not on those ten lines, you are still carrying, whether you know it or not. My tests were a small specification of the problem, and the code rose precisely to its level — and stopped there.

So the ritual holds, and so does its fine print:

Your code is exactly as good as your tests — and your tests are exactly as good as the questions you thought to ask.

So how do I ask better questions than those ten examples? By specifying properties instead of guessing more inputs. That is property-based testing—and, maybe, the next article.

END OF FILE
user@inkspell:~/blog $
$ cd ..
phone down  ·  breathe  ·  begin
☸︎
ai ☸︎ tdd ☸︎ testing

The Code I Didn't Know How to Write

☸︎

Twelve years ago a strict red-green-refactor ritual carried me to a solution I didn't know how to design. The trick wasn't thinking harder. It was thinking on schedule.

session · 8 min sitting · July 24, 2026
☸︎

The algorithm deserved a better programmer. Test-driven development made do by rationing the one it had.

— enter the practice —

In September 2014 I sat down with an algorithm exercise: given an array of integers, find the slice — any run of consecutive elements — whose sum lands closest to zero. There was a performance bound that ruled out anything lazy: N·log(N), if that means something to you; “fast even on huge arrays” if it doesn’t. I had never solved this kind of problem before.

And I made it harder with one rule: perform the ritual properly. Test-driven development, by the book. Write a failing test (that’s the “red”). Write the least code that makes it pass (the “green”). Refactor. Repeat. No sketching the algorithm up front, no building ahead of the tests, no fixing anything — even things I could already see were broken — until a test asked for it.

I want to be honest about how that rule felt: like pressure. Minutes in, I already knew things about the code that would need fixing sooner or later. The ritual kept saying: not now. Your job right now is one failing test.

By the end, I had a solution inside the required bounds. That isn’t the strange part. The strange part was the feeling that came with the result. The code was terse — so terse that reading it didn’t tell me how it worked. But it did work, and the tests kept saying so. Working code beyond my understanding, sitting in my own repository: it felt less like something I had written and more like something I had been handed. I didn’t know it at the time, but that feeling is what good engineering feels like.

I never solved the whole problem

The first test was almost an insult:

@Test
public void testOneElement() {
    assertThat(solution(new int[] {1}), is(1));
}

One element. The slice is the element. And the first implementation was exactly as heroic as the test demanded — return a[0]; — not an abs() in sight, because no test had asked for one yet.

Then two elements. Then three, where mixed signs start to bite, and where it turned out I had misread the requirements. The commit messages from that evening keep me honest. One reads, “Maybe you don’t get the requirements right the first time, but by testing you are very aware of what you are implementing.” Two minutes later: “More tests failing for no reason at all. Maybe one good reason would be using Math.max() instead of Math.min().”

About an hour of small honest steps later I had a general solution: a brute force — try every slice, keep the best. Correct on everything I threw at it, and nowhere near the required speed. The next commit says, “time to put our thinking caps on :)”.

Here is what did not happen next: I did not drop the ritual because the real problem had finally arrived. The commit that follows asks the only question the ritual permits, and gives the only answer it accepts: “So what do we do next? We write a test of course.” So I wrote one — a crude timing test: run the solution at two sizes, complain if the time grew too fast. In its final form it looks like this:

@Test
public void testComplexityIsCloseToN() {
    final int BASE = 5000000;
    final int SIZE_MULTIPLIER = 4;
    int[] low = new int[BASE];
    int[] high = new int[BASE*SIZE_MULTIPLIER];

    long lowTime = timeSolution(low);
    long highTime = timeSolution(high);
    long timeMultiplier = round((double) highTime / lowTime);
    assertThat("The complexity should be close to N (which is N^1) ~ 1",
                    approximateExponent(timeMultiplier, SIZE_MULTIPLIER), is(1L));
}

Wall-clock time and a rounded exponent — benchmarking purists may want to sit down. But it failed while the code was too slow and passed once it was not, and that was all the ritual needed. Performance stopped being an opinion and became a failing test.

Three questions, never all at once

The whole discipline fits in three questions. Red: what should work next? Green: what is the least code that makes it work? Refactor: how do I make this better without changing what it does? At any moment I was allowed to hold exactly one.

There is an obvious objection, and I half-believe it to this day: tiny steps cannot escape a local maximum — polishing a dead end only gets you a shinier dead end. A brute force built of small honest steps is still a brute force, and no amount of refusing-to-think turns it into a clever algorithm. True. It is also not what happened here, because the rule was never “don’t think.” It was “don’t think yet.”

The two real ideas in the final solution — a caching trick, then an ordered set — both arrived on demand, summoned by that failing performance test, and both were validated against everything I had written down so far.

The next morning produced my favorite pair of commits, seven minutes apart. 08:30: “So I think I am done. Let’s run the test and see. Oh my..” 08:37: “Ok. Forgot my abs(). All fixed running all tests and all is green! This is unexpected!”

So, not less thinking. Less thinking per step, more thinking on demand, and never all the thinking at once.

A passing test is a thought you get to put down

Why write tests at all? The answer I keep arriving at: to be able to refactor without fear. But underneath the fearlessness sits something more basic. Every passing test is one less concern I have to keep in my head.

Refactoring narrowed my attention to one question: is this structure better? The suite handled the cases I had already covered.

That is what the ritual had been rationing all along. Not effort — attention. Each phase loads a small set of concerns, and, just as important, unloads the rest onto something that holds them more reliably than I do.

Why this sounds like an AI problem

You may recognize the shape of this from somewhere else. A person can hold only so much in mind at once; a language model can only make good use of so much context. These are not the same mechanism, and I won’t pretend they are. But they fail the same way: pile everything that might be relevant onto the desk, and the answer to the question that is actually relevant gets worse.

The fix I stumbled into in 2014 looks, to me, like the general one. Write what you know down once, in a durable place — tests, notes, a specification. Then, for each decision, bring only the slice that decision needs onto the desk. Red pulls one behavior from the pile. Green pulls one failing test. Refactor pulls a design question and leaves correctness to the suite, which is exactly where it was written down.

Which is why the lesson here is not “make agents do TDD.” It sits one level higher: a good process breaks a problem too large to reason about all at once into smaller problems that can be solved one at a time.

In 2014, the one solving those problems was me on a September evening, out of my depth. Increasingly, that role can belong to an agent with a bounded context window. In either case, the job is not to foresee the finished design. It is to solve the problem at hand with enough context, while the tests protect what has already been implemented.

Your code is exactly as good as your tests

The final commit reads, “And we are finished refactoring.. Shiny, Shiny!” — the sound of a man admiring code he doesn’t entirely believe he wrote.

That is where I left the repository: finished, refactored, and green.

While I was preparing this article, an AI agent doing the background research did something no 2014 version of me ever did. It generated twenty thousand random arrays and compared my solution’s answers with those from a separate brute-force solver. If you’ve heard of property-based testing, this is that, done informally.

The verdict: the code I could not explain disagrees with the truth on roughly a third of random inputs. Not an exotic corner case. A third. The fix, once you see it, is a few lines. I have not applied it — the repository is public, commits and suite and bug all still in residence, if you would like to meet them.

The bug does not ruin the story. It completes it. The suite did exactly what I built it to do: it protected every behavior I thought to write down through every refactor. It never promised more. A green bar is not a certificate of correctness. It is the list of things you are allowed to forget. Here is that list, in its entirety:

assertThat(solution(1), is(1));
assertThat(solution(-1), is(1));
assertThat(solution(1, 2), is(1));
assertThat(solution(-1, 2), is(1));
assertThat(solution(-1, 2, 3), is(1));
assertThat(solution(-1, 1, -1), is(0));
assertThat(solution(5, 2, -1), is(1));
assertThat(solution(1, 2, -1, 4), is(1));
assertThat(solution(5, 2, -1, -1), is(0));
assertThat(solution(2, -4, 6, -3, 9), is(1));

Ten lines. Everything not on those ten lines, you are still carrying, whether you know it or not. My tests were a small specification of the problem, and the code rose precisely to its level — and stopped there.

So the ritual holds, and so does its fine print:

Your code is exactly as good as your tests — and your tests are exactly as good as the questions you thought to ask.

So how do I ask better questions than those ten examples? By specifying properties instead of guessing more inputs. That is property-based testing—and, maybe, the next article.

☸︎
the session ends
learn  ·  unlearn  ·  return
return when ready
☠︎ LARTS-SERVER BBS v2.3.1 — root console
⚠︎ Unauthorised access will be met with creative, legally ambiguous, and frankly disproportionate solutions.
Last login: 2026-07-24 00:00:00 from somewhere you'll regret  ·  session pid 1354
⚡ luser activity is being monitored. it always was. 3 LARTs administered today.
☠︎ BOFH EXCUSE #1144 :: the SCSI chain was haunted
root@larts-server:/var/rants # cat refusing-to-think.txt  # and what was your username again?
SUBJECT: The Code I Didn't Know How to Write
DATE: 2026-07-24 00:00:00 READ: ~8 min of your billable downtime WORDS: 1599
TAGS: [ai] [tdd] [testing]
// Twelve years ago a strict red-green-refactor ritual carried me to a solution I didn't know how to design. The trick wasn't thinking harder. It was thinking on schedule.
☠︎ ::: BEGIN TRANSMISSION — touch nothing ::: ☠︎
⚠︎ PRIORITY MESSAGE FROM THE OPERATOR ⚠︎

The algorithm deserved a better programmer. Test-driven development made do by rationing the one it had.

In September 2014 I sat down with an algorithm exercise: given an array of integers, find the slice — any run of consecutive elements — whose sum lands closest to zero. There was a performance bound that ruled out anything lazy: N·log(N), if that means something to you; “fast even on huge arrays” if it doesn’t. I had never solved this kind of problem before.

And I made it harder with one rule: perform the ritual properly. Test-driven development, by the book. Write a failing test (that’s the “red”). Write the least code that makes it pass (the “green”). Refactor. Repeat. No sketching the algorithm up front, no building ahead of the tests, no fixing anything — even things I could already see were broken — until a test asked for it.

I want to be honest about how that rule felt: like pressure. Minutes in, I already knew things about the code that would need fixing sooner or later. The ritual kept saying: not now. Your job right now is one failing test.

By the end, I had a solution inside the required bounds. That isn’t the strange part. The strange part was the feeling that came with the result. The code was terse — so terse that reading it didn’t tell me how it worked. But it did work, and the tests kept saying so. Working code beyond my understanding, sitting in my own repository: it felt less like something I had written and more like something I had been handed. I didn’t know it at the time, but that feeling is what good engineering feels like.

I never solved the whole problem

The first test was almost an insult:

@Test
public void testOneElement() {
    assertThat(solution(new int[] {1}), is(1));
}

One element. The slice is the element. And the first implementation was exactly as heroic as the test demanded — return a[0]; — not an abs() in sight, because no test had asked for one yet.

Then two elements. Then three, where mixed signs start to bite, and where it turned out I had misread the requirements. The commit messages from that evening keep me honest. One reads, “Maybe you don’t get the requirements right the first time, but by testing you are very aware of what you are implementing.” Two minutes later: “More tests failing for no reason at all. Maybe one good reason would be using Math.max() instead of Math.min().”

About an hour of small honest steps later I had a general solution: a brute force — try every slice, keep the best. Correct on everything I threw at it, and nowhere near the required speed. The next commit says, “time to put our thinking caps on :)”.

Here is what did not happen next: I did not drop the ritual because the real problem had finally arrived. The commit that follows asks the only question the ritual permits, and gives the only answer it accepts: “So what do we do next? We write a test of course.” So I wrote one — a crude timing test: run the solution at two sizes, complain if the time grew too fast. In its final form it looks like this:

@Test
public void testComplexityIsCloseToN() {
    final int BASE = 5000000;
    final int SIZE_MULTIPLIER = 4;
    int[] low = new int[BASE];
    int[] high = new int[BASE*SIZE_MULTIPLIER];

    long lowTime = timeSolution(low);
    long highTime = timeSolution(high);
    long timeMultiplier = round((double) highTime / lowTime);
    assertThat("The complexity should be close to N (which is N^1) ~ 1",
                    approximateExponent(timeMultiplier, SIZE_MULTIPLIER), is(1L));
}

Wall-clock time and a rounded exponent — benchmarking purists may want to sit down. But it failed while the code was too slow and passed once it was not, and that was all the ritual needed. Performance stopped being an opinion and became a failing test.

Three questions, never all at once

The whole discipline fits in three questions. Red: what should work next? Green: what is the least code that makes it work? Refactor: how do I make this better without changing what it does? At any moment I was allowed to hold exactly one.

There is an obvious objection, and I half-believe it to this day: tiny steps cannot escape a local maximum — polishing a dead end only gets you a shinier dead end. A brute force built of small honest steps is still a brute force, and no amount of refusing-to-think turns it into a clever algorithm. True. It is also not what happened here, because the rule was never “don’t think.” It was “don’t think yet.”

The two real ideas in the final solution — a caching trick, then an ordered set — both arrived on demand, summoned by that failing performance test, and both were validated against everything I had written down so far.

The next morning produced my favorite pair of commits, seven minutes apart. 08:30: “So I think I am done. Let’s run the test and see. Oh my..” 08:37: “Ok. Forgot my abs(). All fixed running all tests and all is green! This is unexpected!”

So, not less thinking. Less thinking per step, more thinking on demand, and never all the thinking at once.

A passing test is a thought you get to put down

Why write tests at all? The answer I keep arriving at: to be able to refactor without fear. But underneath the fearlessness sits something more basic. Every passing test is one less concern I have to keep in my head.

Refactoring narrowed my attention to one question: is this structure better? The suite handled the cases I had already covered.

That is what the ritual had been rationing all along. Not effort — attention. Each phase loads a small set of concerns, and, just as important, unloads the rest onto something that holds them more reliably than I do.

Why this sounds like an AI problem

You may recognize the shape of this from somewhere else. A person can hold only so much in mind at once; a language model can only make good use of so much context. These are not the same mechanism, and I won’t pretend they are. But they fail the same way: pile everything that might be relevant onto the desk, and the answer to the question that is actually relevant gets worse.

The fix I stumbled into in 2014 looks, to me, like the general one. Write what you know down once, in a durable place — tests, notes, a specification. Then, for each decision, bring only the slice that decision needs onto the desk. Red pulls one behavior from the pile. Green pulls one failing test. Refactor pulls a design question and leaves correctness to the suite, which is exactly where it was written down.

Which is why the lesson here is not “make agents do TDD.” It sits one level higher: a good process breaks a problem too large to reason about all at once into smaller problems that can be solved one at a time.

In 2014, the one solving those problems was me on a September evening, out of my depth. Increasingly, that role can belong to an agent with a bounded context window. In either case, the job is not to foresee the finished design. It is to solve the problem at hand with enough context, while the tests protect what has already been implemented.

Your code is exactly as good as your tests

The final commit reads, “And we are finished refactoring.. Shiny, Shiny!” — the sound of a man admiring code he doesn’t entirely believe he wrote.

That is where I left the repository: finished, refactored, and green.

While I was preparing this article, an AI agent doing the background research did something no 2014 version of me ever did. It generated twenty thousand random arrays and compared my solution’s answers with those from a separate brute-force solver. If you’ve heard of property-based testing, this is that, done informally.

The verdict: the code I could not explain disagrees with the truth on roughly a third of random inputs. Not an exotic corner case. A third. The fix, once you see it, is a few lines. I have not applied it — the repository is public, commits and suite and bug all still in residence, if you would like to meet them.

The bug does not ruin the story. It completes it. The suite did exactly what I built it to do: it protected every behavior I thought to write down through every refactor. It never promised more. A green bar is not a certificate of correctness. It is the list of things you are allowed to forget. Here is that list, in its entirety:

assertThat(solution(1), is(1));
assertThat(solution(-1), is(1));
assertThat(solution(1, 2), is(1));
assertThat(solution(-1, 2), is(1));
assertThat(solution(-1, 2, 3), is(1));
assertThat(solution(-1, 1, -1), is(0));
assertThat(solution(5, 2, -1), is(1));
assertThat(solution(1, 2, -1, 4), is(1));
assertThat(solution(5, 2, -1, -1), is(0));
assertThat(solution(2, -4, 6, -3, 9), is(1));

Ten lines. Everything not on those ten lines, you are still carrying, whether you know it or not. My tests were a small specification of the problem, and the code rose precisely to its level — and stopped there.

So the ritual holds, and so does its fine print:

Your code is exactly as good as your tests — and your tests are exactly as good as the questions you thought to ask.

So how do I ask better questions than those ten examples? By specifying properties instead of guessing more inputs. That is property-based testing—and, maybe, the next article.

☠︎ ::: END OF FILE — this never happened ::: ☠︎
[2026-07-24 00:00:00] SYSTEM: helpdesk calls handled with 'have you tried turning it off': 100%
[2026-07-24 00:00:00] SYSTEM: your read has been logged against your permanent record.
root@larts-server:/var/rants # logout  # don't let the airlock hit you
⛤︎
the circle is opened
☾ return to the archive ☽
☾ ai ☽ ☾ tdd ☽ ☾ testing ☽
⛤︎   VERITAS   ⛤︎

The Code I Didn't Know How to Write

☾   ⛤︎   ☽

Twelve years ago a strict red-green-refactor ritual carried me to a solution I didn't know how to design. The trick wasn't thinking harder. It was thinking on schedule.

July 24, 2026 ⛤︎ 8 min rite ⛤︎ 1599 words
☾   ⛤︎   ☽

The algorithm deserved a better programmer. Test-driven development made do by rationing the one it had.

In September 2014 I sat down with an algorithm exercise: given an array of integers, find the slice — any run of consecutive elements — whose sum lands closest to zero. There was a performance bound that ruled out anything lazy: N·log(N), if that means something to you; “fast even on huge arrays” if it doesn’t. I had never solved this kind of problem before.

And I made it harder with one rule: perform the ritual properly. Test-driven development, by the book. Write a failing test (that’s the “red”). Write the least code that makes it pass (the “green”). Refactor. Repeat. No sketching the algorithm up front, no building ahead of the tests, no fixing anything — even things I could already see were broken — until a test asked for it.

I want to be honest about how that rule felt: like pressure. Minutes in, I already knew things about the code that would need fixing sooner or later. The ritual kept saying: not now. Your job right now is one failing test.

By the end, I had a solution inside the required bounds. That isn’t the strange part. The strange part was the feeling that came with the result. The code was terse — so terse that reading it didn’t tell me how it worked. But it did work, and the tests kept saying so. Working code beyond my understanding, sitting in my own repository: it felt less like something I had written and more like something I had been handed. I didn’t know it at the time, but that feeling is what good engineering feels like.

I never solved the whole problem

The first test was almost an insult:

@Test
public void testOneElement() {
    assertThat(solution(new int[] {1}), is(1));
}

One element. The slice is the element. And the first implementation was exactly as heroic as the test demanded — return a[0]; — not an abs() in sight, because no test had asked for one yet.

Then two elements. Then three, where mixed signs start to bite, and where it turned out I had misread the requirements. The commit messages from that evening keep me honest. One reads, “Maybe you don’t get the requirements right the first time, but by testing you are very aware of what you are implementing.” Two minutes later: “More tests failing for no reason at all. Maybe one good reason would be using Math.max() instead of Math.min().”

About an hour of small honest steps later I had a general solution: a brute force — try every slice, keep the best. Correct on everything I threw at it, and nowhere near the required speed. The next commit says, “time to put our thinking caps on :)”.

Here is what did not happen next: I did not drop the ritual because the real problem had finally arrived. The commit that follows asks the only question the ritual permits, and gives the only answer it accepts: “So what do we do next? We write a test of course.” So I wrote one — a crude timing test: run the solution at two sizes, complain if the time grew too fast. In its final form it looks like this:

@Test
public void testComplexityIsCloseToN() {
    final int BASE = 5000000;
    final int SIZE_MULTIPLIER = 4;
    int[] low = new int[BASE];
    int[] high = new int[BASE*SIZE_MULTIPLIER];

    long lowTime = timeSolution(low);
    long highTime = timeSolution(high);
    long timeMultiplier = round((double) highTime / lowTime);
    assertThat("The complexity should be close to N (which is N^1) ~ 1",
                    approximateExponent(timeMultiplier, SIZE_MULTIPLIER), is(1L));
}

Wall-clock time and a rounded exponent — benchmarking purists may want to sit down. But it failed while the code was too slow and passed once it was not, and that was all the ritual needed. Performance stopped being an opinion and became a failing test.

Three questions, never all at once

The whole discipline fits in three questions. Red: what should work next? Green: what is the least code that makes it work? Refactor: how do I make this better without changing what it does? At any moment I was allowed to hold exactly one.

There is an obvious objection, and I half-believe it to this day: tiny steps cannot escape a local maximum — polishing a dead end only gets you a shinier dead end. A brute force built of small honest steps is still a brute force, and no amount of refusing-to-think turns it into a clever algorithm. True. It is also not what happened here, because the rule was never “don’t think.” It was “don’t think yet.”

The two real ideas in the final solution — a caching trick, then an ordered set — both arrived on demand, summoned by that failing performance test, and both were validated against everything I had written down so far.

The next morning produced my favorite pair of commits, seven minutes apart. 08:30: “So I think I am done. Let’s run the test and see. Oh my..” 08:37: “Ok. Forgot my abs(). All fixed running all tests and all is green! This is unexpected!”

So, not less thinking. Less thinking per step, more thinking on demand, and never all the thinking at once.

A passing test is a thought you get to put down

Why write tests at all? The answer I keep arriving at: to be able to refactor without fear. But underneath the fearlessness sits something more basic. Every passing test is one less concern I have to keep in my head.

Refactoring narrowed my attention to one question: is this structure better? The suite handled the cases I had already covered.

That is what the ritual had been rationing all along. Not effort — attention. Each phase loads a small set of concerns, and, just as important, unloads the rest onto something that holds them more reliably than I do.

Why this sounds like an AI problem

You may recognize the shape of this from somewhere else. A person can hold only so much in mind at once; a language model can only make good use of so much context. These are not the same mechanism, and I won’t pretend they are. But they fail the same way: pile everything that might be relevant onto the desk, and the answer to the question that is actually relevant gets worse.

The fix I stumbled into in 2014 looks, to me, like the general one. Write what you know down once, in a durable place — tests, notes, a specification. Then, for each decision, bring only the slice that decision needs onto the desk. Red pulls one behavior from the pile. Green pulls one failing test. Refactor pulls a design question and leaves correctness to the suite, which is exactly where it was written down.

Which is why the lesson here is not “make agents do TDD.” It sits one level higher: a good process breaks a problem too large to reason about all at once into smaller problems that can be solved one at a time.

In 2014, the one solving those problems was me on a September evening, out of my depth. Increasingly, that role can belong to an agent with a bounded context window. In either case, the job is not to foresee the finished design. It is to solve the problem at hand with enough context, while the tests protect what has already been implemented.

Your code is exactly as good as your tests

The final commit reads, “And we are finished refactoring.. Shiny, Shiny!” — the sound of a man admiring code he doesn’t entirely believe he wrote.

That is where I left the repository: finished, refactored, and green.

While I was preparing this article, an AI agent doing the background research did something no 2014 version of me ever did. It generated twenty thousand random arrays and compared my solution’s answers with those from a separate brute-force solver. If you’ve heard of property-based testing, this is that, done informally.

The verdict: the code I could not explain disagrees with the truth on roughly a third of random inputs. Not an exotic corner case. A third. The fix, once you see it, is a few lines. I have not applied it — the repository is public, commits and suite and bug all still in residence, if you would like to meet them.

The bug does not ruin the story. It completes it. The suite did exactly what I built it to do: it protected every behavior I thought to write down through every refactor. It never promised more. A green bar is not a certificate of correctness. It is the list of things you are allowed to forget. Here is that list, in its entirety:

assertThat(solution(1), is(1));
assertThat(solution(-1), is(1));
assertThat(solution(1, 2), is(1));
assertThat(solution(-1, 2), is(1));
assertThat(solution(-1, 2, 3), is(1));
assertThat(solution(-1, 1, -1), is(0));
assertThat(solution(5, 2, -1), is(1));
assertThat(solution(1, 2, -1, 4), is(1));
assertThat(solution(5, 2, -1, -1), is(0));
assertThat(solution(2, -4, 6, -3, 9), is(1));

Ten lines. Everything not on those ten lines, you are still carrying, whether you know it or not. My tests were a small specification of the problem, and the code rose precisely to its level — and stopped there.

So the ritual holds, and so does its fine print:

Your code is exactly as good as your tests — and your tests are exactly as good as the questions you thought to ask.

So how do I ask better questions than those ten examples? By specifying properties instead of guessing more inputs. That is property-based testing—and, maybe, the next article.

☾  ⛤︎  ────────  ⛤︎  ☽
so it is written · so it is bound
☾ return to the archive ☽
inkspell
© 2026 inkspell RSS