A shipping quote API should charge EUR 5.90 for a domestic parcel weighing up to and including one kilogram. Instead, it returns EUR 9.90 when the weight is exactly 1,000 grams.
I prepared a small Laravel playground repository with this bug and a deliberately long commit history. The application is simple enough to follow, while the history makes it useful for trying a debugging technique that also works on much larger projects: combining a focused Pest test with git bisect.
Reproducing the shipping price bug
Clone and prepare the repository, then start Laravel's development server on port 8001:
git clone https://github.com/maiobarbero/laravel-pest-bisect.git
cd laravel-pest-bisect
composer setup
php artisan serve --port=8001
In another terminal, request a quote for a 1,000-gram domestic parcel:
curl --request POST http://localhost:8001/api/shipping/quote \
--header 'Content-Type: application/json' \
--data '{"weight_grams":1000,"zone":"domestic"}'
The response identifies the parcel as small, but returns 990 in price_cents:
{
"weight_grams": 1000,
"zone": "domestic",
"price_cents": 990,
"currency": "EUR",
"weight_band": "small"
}
We know the expected price is 590 cents, and we know the current version is wrong. What we don't know yet is which commit changed the behaviour.
One option is to check out each earlier commit, repeat the request, and continue until the response changes. This is manageable with a handful of commits. With hundreds or thousands of commits, it becomes slow enough that it is rarely a practical approach.
How Git bisect narrows the search
git bisect uses a binary search through the commit history. We give Git one commit where the behaviour is broken and one older commit where it still works. Git checks out a commit near the middle and asks whether that revision is good or bad, then discards the half of the range that cannot contain the regression.
The number of candidates is roughly halved after every check. Searching 1,000 commits therefore takes about 10 checks in the ideal case, rather than testing every commit one by one.
The check can be performed manually, but this example has a behaviour we can express as an automated test. That allows Git to run the entire search for us.
Turning the bug into a Pest test
Create tests/Feature/ShippingQuoteTest.php with a feature test for the one-kilogram boundary:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\TestCase;
uses(TestCase::class);
it('applies the small parcel tariff up to one kilogram included', function () {
$this->postJson('/api/shipping/quote', [
'weight_grams' => 1000,
'zone' => 'domestic',
])
->assertOk()
->assertJsonPath('price_cents', 590);
});
The test calls the real API route and records the expected behaviour in one place. Run it by filtering the suite with the test filename:
php artisan test --filter=ShippingQuote
It fails on the current revision because the endpoint returns 990 instead of 590. This failure is useful beyond confirming the bug: a passing test exits with status code 0, while a failing test exits with a non-zero status. git bisect run uses that status to classify every revision automatically as good or bad.
Keep the new test uncommitted while running the bisect. It needs to remain in the working tree as Git checks out each revision in the search range. Before starting, check git status and make sure there are no unrelated changes that could affect the result.
Running the automated bisect
Start a bisect session and mark the current commit as bad:
git bisect start
git bisect bad
For this playground, commit 06bc68c5 is a known good revision. It contains the shipping quote endpoint before the regression was introduced:
git bisect good 06bc68c5
Git now has both ends of the range. Pass the focused Pest command to git bisect run:
git bisect run php artisan test --filter=ShippingQuote
Git repeatedly checks out a candidate commit and runs the test. A pass moves the good boundary forward; a failure moves the bad boundary backward. It continues until only the first failing revision remains:
9e8d96614e62c10f47efd915614836b8b42c74e9 is the first bad commit
commit 9e8d96614e62c10f47efd915614836b8b42c74e9
Author: Maio Barbero
Date: Mon Aug 31 10:15:17 2026 +0200
26 refactor shipping prices using a match expression
app/Services/ShippingPriceCalculator.php | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
bisect found first bad commit
The search points to commit 9e8d966, a refactor of ShippingPriceCalculator. Its diff contains the boundary mistake:
return match (true) {
$weightGrams < 1000 => 590,
$weightGrams <= 5000 => 990,
default => 1590,
};
The small-parcel condition changed from <= 1000 to < 1000. A parcel weighing exactly 1,000 grams no longer matches the first arm and falls through to the medium price. The rest of the refactor is unrelated to the failure.
Once the investigation is complete, leave bisect mode and return to the branch and commit that were checked out before the search:
git bisect reset
At this point the failing Pest test has done two jobs. It found the regression in the existing history, and it can remain in the suite as a boundary test to prevent the same comparison error from returning after the fix.
Top comments (0)