<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: tosane932</title>
    <description>The latest articles on DEV Community by tosane932 (@tosane932).</description>
    <link>https://dev.to/tosane932</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4044609%2F1c89177e-ec1e-4634-8d68-81ba7993e406.jpg</url>
      <title>DEV Community: tosane932</title>
      <link>https://dev.to/tosane932</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tosane932"/>
    <language>en</language>
    <item>
      <title>🏗️ Before Letting Guests In, I Protected Existing Data: Building Dataset Boundaries with DB Migrations and 114 pytest Tests</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Sat, 05 Sep 2026 23:17:59 +0000</pubDate>
      <link>https://dev.to/tosane932/before-letting-guests-in-i-protected-existing-data-building-dataset-boundaries-with-db-57am</link>
      <guid>https://dev.to/tosane932/before-letting-guests-in-i-protected-existing-data-building-dataset-boundaries-with-db-57am</guid>
      <description>&lt;h2&gt;
  
  
  Hello from Japan 🇯🇵
&lt;/h2&gt;

&lt;p&gt;I wanted to turn my Flask portfolio from something people could only &lt;strong&gt;look at&lt;/strong&gt; into something they could actually &lt;strong&gt;try for themselves&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That was why I started building a Guest Demo mode.&lt;/p&gt;

&lt;p&gt;But before creating a single Guest screen, I ran into a much more important question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If I allow Guests to write data, how do I make sure they can never touch the existing Admin data?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question turned what looked like a small feature into a database migration project.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;h2&gt;
  
  
  🚀 Update — September 7, 2026
&lt;/h2&gt;

&lt;p&gt;The Guest Demo that this series was building toward is now live.&lt;/p&gt;

&lt;p&gt;You can try &lt;code&gt;sales_data_app&lt;/code&gt; directly without creating an account:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bakery-salesdata.onrender.com/" rel="noopener noreferrer"&gt;https://bakery-salesdata.onrender.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article documents &lt;strong&gt;Stage 1&lt;/strong&gt;, when the Guest Demo itself had not been opened yet. The descriptions below intentionally preserve the state of the project at that point in development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In three lines&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;🛡️ Before building the Guest Demo, I introduced a &lt;code&gt;Dataset&lt;/code&gt; boundary to protect existing Admin data.&lt;/li&gt;
&lt;li&gt;🏗️ To migrate existing Products without breaking them, I used an &lt;strong&gt;Expand → Migrate → Contract&lt;/strong&gt; flow: add the column, backfill existing data, then enforce &lt;code&gt;NOT NULL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;🧪 I verified the foundation with &lt;strong&gt;114 pytest tests&lt;/strong&gt;, a production backup, post-migration database checks, and production smoke tests before letting Guests in.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧪 Stage 1: Turning a Read-Only Portfolio into Something People Can Actually Try
&lt;/h2&gt;

&lt;p&gt;The application in this article is a sales-management and analysis system I have been building as a personal project.&lt;/p&gt;

&lt;p&gt;At the time of Stage 1, it already included features such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product registration&lt;/li&gt;
&lt;li&gt;Daily sales entry&lt;/li&gt;
&lt;li&gt;Sales rankings&lt;/li&gt;
&lt;li&gt;Charts&lt;/li&gt;
&lt;li&gt;AI-assisted sales analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Until then, however, the application assumed an Admin login.&lt;/p&gt;

&lt;p&gt;Recruiters or engineers could look at the project, but they could not freely interact with it.&lt;/p&gt;

&lt;p&gt;So I wanted to make this possible:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let someone try product registration and sales entry as a Guest without giving them my Admin credentials.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I noticed the danger.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;If I allow Guests to write data, couldn't they also modify the existing Admin data?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before building a Guest login screen, I needed a foundation that could separate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin data from Guest data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As a result, I did not create a single Guest screen in Stage 1.&lt;/p&gt;

&lt;p&gt;Instead, I worked on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataset design and database migrations for protecting the existing production data.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Goal of Stage 1
&lt;/h2&gt;

&lt;p&gt;The goal was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Move the existing Admin data into a Dataset boundary so that future Guest Datasets could be added without mixing their data together.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Guest functionality itself would come later.&lt;/p&gt;

&lt;p&gt;First, I wanted the existing data to become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL
│
└─ Admin Dataset
    │
    └─ Product
        │
        └─ DailySales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intended future structure was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL
│
├─ Admin Dataset
│   └─ Product
│       └─ DailySales
│
├─ Guest Dataset A
│   └─ Product
│       └─ DailySales
│
└─ Guest Dataset B
    └─ Product
        └─ DailySales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea was to isolate data by Dataset.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Couldn't Build the Guest Feature First
&lt;/h2&gt;

&lt;p&gt;Before this change, &lt;code&gt;Product&lt;/code&gt; had no information answering:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Whose data is this Product?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id = 1
name = White Bread
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database could not tell whether that Product belonged to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin&lt;/li&gt;
&lt;li&gt;Guest A&lt;/li&gt;
&lt;li&gt;Guest B&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I added Guest functionality in that state, I could end up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Admin Products
Guest A Products
Guest B Products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;all sitting on the same shelf.&lt;/p&gt;

&lt;p&gt;So I decided to give every Product an &lt;strong&gt;ownership boundary&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thinking of a Dataset as a Room
&lt;/h3&gt;

&lt;p&gt;This was the mental model I used:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL = the building&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataset = a room&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product = an item stored inside that room&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dataset_id = a name tag showing which room the Product belongs to&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Product structure becomes something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product
├─ id
├─ name
├─ price
└─ dataset_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;dataset_id&lt;/code&gt; points to the Admin Dataset:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This Product belongs to the Admin room.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Future Guest Products could be separated in the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Admin Product
→ dataset_id points to Admin Dataset

Guest A Product
→ dataset_id points to Guest Dataset A

Guest B Product
→ dataset_id points to Guest Dataset B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Adding the Dataset Model
&lt;/h2&gt;

&lt;p&gt;First, I introduced a &lt;code&gt;Dataset&lt;/code&gt; model.&lt;/p&gt;

&lt;p&gt;The general structure looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datasets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as_uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;system_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="n"&gt;last_activity_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="n"&gt;absolute_expires_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kind&lt;/code&gt; can be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;admin
guest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Admin Dataset follows this rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kind = admin
system_key = admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Guest Dataset follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kind = guest
system_key = NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guests were not actually being used yet in Stage 1.&lt;/p&gt;

&lt;p&gt;I was only preparing the structure for adding them later.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Wanted the Database Itself to Enforce the Rules
&lt;/h2&gt;

&lt;p&gt;One thing I deliberately avoided was relying only on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"The Python code will probably use the values correctly."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, &lt;code&gt;Dataset.kind&lt;/code&gt; should only allow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;admin
guest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I also added a database-level &lt;code&gt;CHECK&lt;/code&gt; constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CheckConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind IN (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;guest&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ck_datasets_kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also added database rules for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Admin → system_key = admin

Guest → system_key = NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My mental model was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Validation in Python = the reception desk at the building&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database CHECK constraint = a rule built into the building itself&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even if the reception desk accidentally allows an invalid value through, the database still has one final chance to reject it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Giving Every Product an Ownership Tag
&lt;/h2&gt;

&lt;p&gt;Next, I added &lt;code&gt;dataset_id&lt;/code&gt; to &lt;code&gt;Product&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The final model contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;dataset_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as_uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datasets.id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ondelete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CASCADE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means every Product must belong to a Dataset.&lt;/p&gt;

&lt;p&gt;My mental model was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"No Product without a name tag is allowed into the warehouse."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But there was a problem.&lt;/p&gt;

&lt;p&gt;The production database already contained Products.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Couldn't Just Add &lt;code&gt;NOT NULL&lt;/code&gt; Immediately
&lt;/h2&gt;

&lt;p&gt;Before the change, &lt;code&gt;Product&lt;/code&gt; did not even have a:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dataset_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;column.&lt;/p&gt;

&lt;p&gt;What would happen if I immediately added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dataset_id NOT NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to a table that already contained Products?&lt;/p&gt;

&lt;p&gt;The existing rows would have no value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Existing Product
↓
Add dataset_id
↓
Existing Product has no Dataset
↓
NOT NULL violation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Wanting the final column to be &lt;code&gt;NOT NULL&lt;/code&gt; does not mean it is safe to make it &lt;code&gt;NOT NULL&lt;/code&gt; from the beginning.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I also had to think about how the existing data would move into the new structure.&lt;/p&gt;

&lt;p&gt;So I split the migration into stages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Expand → Migrate → Contract
&lt;/h2&gt;

&lt;p&gt;The migration ended up following this flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Expand
   Add dataset_id while allowing NULL

2. Migrate
   Assign the Admin Dataset to all existing Products

3. Contract
   Make dataset_id NOT NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is close to the:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expand → Migrate → Contract&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;pattern.&lt;/p&gt;

&lt;p&gt;I understood it like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't close the old gate immediately.&lt;/p&gt;

&lt;p&gt;First, build the new route.&lt;/p&gt;

&lt;p&gt;Move the existing data onto that route.&lt;/p&gt;

&lt;p&gt;Confirm that everyone has moved.&lt;/p&gt;

&lt;p&gt;Only then make the old state invalid.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Migration 1: Create Dataset and Backfill Existing Products
&lt;/h2&gt;

&lt;p&gt;The first migration revision was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;c7a1d9e4f2b6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It performed these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the &lt;code&gt;datasets&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;products.dataset_id&lt;/code&gt; while allowing &lt;code&gt;NULL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create the Admin Dataset&lt;/li&gt;
&lt;li&gt;Backfill all existing Products into the Admin Dataset&lt;/li&gt;
&lt;li&gt;Add the foreign key&lt;/li&gt;
&lt;li&gt;Add the index&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What Is a Backfill?
&lt;/h3&gt;

&lt;p&gt;In this migration, backfilling meant:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;adding an Admin Dataset ownership tag to Products that already existed.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before

Product 1  White Bread
Product 2  Croissant
Product 3  Melon Bread

        ↓

Create Admin Dataset

        ↓

Product 1  White Bread
└─ dataset_id → Admin Dataset

Product 2  Croissant
└─ dataset_id → Admin Dataset

Product 3  Melon Bread
└─ dataset_id → Admin Dataset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the existing Products could participate in the new Dataset structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Put Validation Inside the Migration Itself
&lt;/h2&gt;

&lt;p&gt;I did not want the migration logic to be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UPDATE completed
↓
Migration successful
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before modifying the data, I recorded the row counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;product_count_before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_count_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sales_count_before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_count_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daily_sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the backfill, the migration checked things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the Product count remain unchanged?&lt;/li&gt;
&lt;li&gt;Did the DailySales count remain unchanged?&lt;/li&gt;
&lt;li&gt;Were any Products still using &lt;code&gt;dataset_id = NULL&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Did all Products belong to the Admin Dataset?&lt;/li&gt;
&lt;li&gt;Were there any orphan Products pointing to no valid Dataset?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something was wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;stopped the migration.&lt;/p&gt;

&lt;p&gt;One idea became especially important to me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"The migration reached the end" does not mean "the data was migrated correctly."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted row counts and ownership relationships to be part of the definition of success.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration 2: Enforce &lt;code&gt;NOT NULL&lt;/code&gt; at the End
&lt;/h2&gt;

&lt;p&gt;The second migration revision was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f2b6c8d4e1a9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was where:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;products.dataset_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;finally became &lt;code&gt;NOT NULL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But before strengthening the constraint, the migration checked again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does exactly one Admin Dataset exist?&lt;/li&gt;
&lt;li&gt;Are any Products still using &lt;code&gt;dataset_id = NULL&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Does any Product reference a Dataset that does not exist?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after those checks passed did it run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;batch_op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alter_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;existing_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as_uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, the database itself guaranteed that a Product could not exist without a Dataset.&lt;/p&gt;




&lt;h2&gt;
  
  
  Changing Only the Database Would Still Be Dangerous
&lt;/h2&gt;

&lt;p&gt;There was another compatibility problem.&lt;/p&gt;

&lt;p&gt;Suppose I changed the database to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dataset_id NOT NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but the old application still created Products like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;New Product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new database would reject the write.&lt;/p&gt;

&lt;p&gt;That creates a dangerous combination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New database
dataset_id NOT NULL

×

Old application
creates Product without dataset_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;A database migration has to consider not only the database itself, but also the application code that may be running against it during the transition.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I updated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product creation&lt;/li&gt;
&lt;li&gt;Seed logic&lt;/li&gt;
&lt;li&gt;Product creation inside pytest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;to understand Datasets as well.&lt;/p&gt;




&lt;h2&gt;
  
  
  New Products Also Receive the Admin Dataset
&lt;/h2&gt;

&lt;p&gt;When registering a new Product, the application retrieves the Admin Dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;admin_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_admin_dataset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and creates the Product with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;admin_dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every newly registered Admin Product therefore has a &lt;code&gt;dataset_id&lt;/code&gt; that points to the Admin Dataset.&lt;/p&gt;

&lt;p&gt;And if the Admin Dataset is unexpectedly missing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the application does not silently create a new one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Product creation fails instead.&lt;/p&gt;

&lt;p&gt;I wanted the abnormal state to fail safely rather than pretending everything was normal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Updating &lt;code&gt;seed_demo.py&lt;/code&gt; for Dataset Support
&lt;/h2&gt;

&lt;p&gt;The production startup process also runs &lt;code&gt;seed_demo.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the seed logic remained on the old structure, it could try to create a Product without a Dataset after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dataset_id NOT NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;was enforced.&lt;/p&gt;

&lt;p&gt;So the seed logic was also updated to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find Admin Dataset
↓
Create Product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the Admin Dataset does not exist, the seed process fails instead of guessing.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Paused for Understanding Checks During the Implementation
&lt;/h2&gt;

&lt;p&gt;This time, I did not ask Codex to implement everything at once and inspect the result only at the end.&lt;/p&gt;

&lt;p&gt;I used a loop like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Investigate
↓
Understand the mechanism
↓
Implement a small part
↓
Verify with pytest
↓
Check my own understanding
↓
Continue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At important points, I stopped and asked myself whether I could explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why Dataset was necessary&lt;/li&gt;
&lt;li&gt;What &lt;code&gt;dataset_id&lt;/code&gt; represented&lt;/li&gt;
&lt;li&gt;Why it could not be &lt;code&gt;NOT NULL&lt;/code&gt; immediately&lt;/li&gt;
&lt;li&gt;What the backfill was doing&lt;/li&gt;
&lt;li&gt;Why the database needed CHECK constraints in addition to Python validation&lt;/li&gt;
&lt;li&gt;Why the migration was split into two revisions&lt;/li&gt;
&lt;li&gt;Why I disabled Auto-Deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My rule became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Not "the code works, so continue," but "I can explain why we're doing this, so continue."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because I was using AI during development, I wanted to avoid increasing implementation speed while leaving my own understanding behind.&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Was GREEN — and I Still Stopped
&lt;/h2&gt;

&lt;p&gt;During the Dataset migration work, the existing pytest suite was already GREEN.&lt;/p&gt;

&lt;p&gt;But when I searched through the test code, I found multiple places still creating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without a Dataset.&lt;/p&gt;

&lt;p&gt;There were about 20 such cases in the normal pytest suite alone.&lt;/p&gt;

&lt;p&gt;So the situation was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The application itself understood Datasets, but test Products were still being created with the old structure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why did the tests still pass?&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;dataset_id&lt;/code&gt; was temporarily nullable during the migration process.&lt;/p&gt;

&lt;p&gt;That meant the old test fixtures could survive.&lt;/p&gt;

&lt;p&gt;So I reviewed all Product creation in the test suite as well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding Dataset-Specific pytest Coverage
&lt;/h2&gt;

&lt;p&gt;I also added tests specifically for Dataset behavior and migration safety.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin Dataset can be created&lt;/li&gt;
&lt;li&gt;Guest Dataset can be created&lt;/li&gt;
&lt;li&gt;Invalid &lt;code&gt;kind&lt;/code&gt; is rejected&lt;/li&gt;
&lt;li&gt;Invalid &lt;code&gt;system_key&lt;/code&gt; for an Admin Dataset is rejected&lt;/li&gt;
&lt;li&gt;A Guest Dataset with a &lt;code&gt;system_key&lt;/code&gt; is rejected&lt;/li&gt;
&lt;li&gt;A Product can belong to a Dataset&lt;/li&gt;
&lt;li&gt;A nonexistent Dataset ID is rejected&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dataset_id = NULL&lt;/code&gt; is rejected after the migration&lt;/li&gt;
&lt;li&gt;Existing Product counts are preserved during migration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;114 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More important than simply seeing GREEN was asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Did this GREEN actually pass through the new Dataset structure?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  I Added &lt;code&gt;ON DELETE CASCADE&lt;/code&gt;, but This Was Not the Final Cleanup Design
&lt;/h2&gt;

&lt;p&gt;The foreign key for &lt;code&gt;Product.dataset_id&lt;/code&gt; includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ondelete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CASCADE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a foundation where deleting a Dataset can also delete the Products belonging to it.&lt;/p&gt;

&lt;p&gt;The intended conceptual cleanup flow for a later stage was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Delete Dataset
↓
Delete Products
↓
Delete DailySales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1 did not yet complete the deletion chain all the way through DailySales.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DailySales is connected through Product.&lt;/p&gt;

&lt;p&gt;This part was intentionally left for a later stage, where Guest Dataset expiration, cleanup behavior, and referential consistency would be addressed together.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Stage 1 was specifically about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;adding a Dataset boundary to existing Admin data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Guest Dataset lifetime management and the complete automatic deletion chain were not finished yet.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  pytest Passed. I Still Didn't Deploy Yet.
&lt;/h2&gt;

&lt;p&gt;All 114 pytest tests passed.&lt;/p&gt;

&lt;p&gt;Normally, that creates a strong temptation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Great. Deploy!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this was a database migration.&lt;/p&gt;

&lt;p&gt;The production PostgreSQL database already contained Admin data I had been using.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚨 &lt;strong&gt;The highest priority was not shipping the new feature quickly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It was protecting the existing data.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From this point onward, I treated the work like a safety-critical change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safety first. ⛑️&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Turning Off Render Auto-Deploy
&lt;/h2&gt;

&lt;p&gt;The Docker startup process for this application ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flask db upgrade
↓
python seed_demo.py
↓
gunicorn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That meant once a Render deployment began:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the production database migration would begin automatically as part of startup.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the first thing I did was disable Auto-Deploy.&lt;/p&gt;

&lt;p&gt;Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merge to GitHub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;did not immediately become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Production DB migration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database remained unchanged until I manually started the deployment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ I deliberately separated:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;merging the code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;starting the production database work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I did not want "merge" to mean "construction begins immediately."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Backing Up the Production Database
&lt;/h2&gt;

&lt;p&gt;Next, I created a backup of the production PostgreSQL database.&lt;/p&gt;

&lt;p&gt;There was one more problem.&lt;/p&gt;

&lt;p&gt;My local &lt;code&gt;pg_dump&lt;/code&gt; was from PostgreSQL 16.&lt;/p&gt;

&lt;p&gt;The production PostgreSQL server was version 18.&lt;/p&gt;

&lt;p&gt;So I used the PostgreSQL 18 Docker image and ran a version-compatible &lt;code&gt;pg_dump&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After creating the backup, I also ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_restore &lt;span class="nt"&gt;--list&lt;/span&gt; backup.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to confirm that the archive could be read.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ This was &lt;strong&gt;not a complete restore test.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What I verified was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;pg_restore&lt;/code&gt; could successfully read the backup archive I had created.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Recording the Production Baseline Before Migration
&lt;/h2&gt;

&lt;p&gt;A backup alone was not enough.&lt;/p&gt;

&lt;p&gt;I also recorded:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;what the production database looked like before the migration.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The baseline was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alembic revision
9d3c1b7e5a42

Product
8 rows
IDs 1–8
Active products: 8

DailySales
56 rows
IDs 1–56

Orphan DailySales
0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These numbers became my post-migration comparison point.&lt;/p&gt;

&lt;p&gt;Instead of saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The data is probably still there."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted to be able to say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before: 8 Products
After:  8 Products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with actual numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Merging the PR Still Did Not Change Production
&lt;/h2&gt;

&lt;p&gt;I merged the changes into &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The diff was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;18 files changed
1301 insertions
15 deletions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But Auto-Deploy remained disabled.&lt;/p&gt;

&lt;p&gt;So at that point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub
→ New code

Render production
→ Still old code and old DB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also updated my local &lt;code&gt;main&lt;/code&gt; and confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nothing to commit, working tree clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Starting the Production Migration
&lt;/h2&gt;

&lt;p&gt;Next, I stopped write operations against the production application and manually started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Manual Deploy
↓
Deploy latest commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Render deployment log showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Running upgrade 9d3c1b7e5a42 -&amp;gt; c7a1d9e4f2b6
Running upgrade c7a1d9e4f2b6 -&amp;gt; f2b6c8d4e1a9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Demo seed skipped: database already contains data.
Starting gunicorn
Your service is live
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, the startup sequence had completed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migration
↓
Seed decision
↓
Gunicorn startup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  I Didn't Stop at "Deploy Succeeded"
&lt;/h2&gt;

&lt;p&gt;Render said the deployment succeeded.&lt;/p&gt;

&lt;p&gt;That still did not prove that the existing data was intact.&lt;/p&gt;

&lt;p&gt;So I inspected the production database directly.&lt;/p&gt;

&lt;p&gt;The result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alembic
f2b6c8d4e1a9

Dataset
Total: 1
Admin: 1
Guest: 0

Product
8 rows
IDs 1–8
dataset_id NULL: 0
Owned by Admin: 8
Orphan Product: 0

DailySales
56 rows
IDs 1–56
Orphan DailySales: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product:    8
DailySales: 56
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product:    8
DailySales: 56
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;I preserved all 8 existing Products and all 56 DailySales records while giving every existing Product an Admin Dataset ownership tag.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Final Step Was a Production Smoke Test
&lt;/h2&gt;

&lt;p&gt;Correct row counts were not enough.&lt;/p&gt;

&lt;p&gt;The application still had to work.&lt;/p&gt;

&lt;p&gt;So I performed a production smoke test on Render.&lt;/p&gt;

&lt;p&gt;I checked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin login&lt;/li&gt;
&lt;li&gt;Product list&lt;/li&gt;
&lt;li&gt;Daily sales entry&lt;/li&gt;
&lt;li&gt;Dashboard&lt;/li&gt;
&lt;li&gt;Sales rankings&lt;/li&gt;
&lt;li&gt;Charts&lt;/li&gt;
&lt;li&gt;AI advice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything worked normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Mean by a Smoke Test
&lt;/h3&gt;

&lt;p&gt;A smoke test is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;checking every detailed condition.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;checking whether the application's major functions are still alive after a change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Using my truck-driving work as an analogy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;pytest = a detailed inspection using an inspection checklist&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smoke test = after maintenance, actually starting the engine and checking that the vehicle can move, steer, and stop&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was the mental model I used.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Also Tested Production Writes
&lt;/h2&gt;

&lt;p&gt;I did not stop with read-only checks.&lt;/p&gt;

&lt;p&gt;I registered a new Product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;明太フランス
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and entered daily sales for it.&lt;/p&gt;

&lt;p&gt;I then confirmed that the new sales appeared correctly in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sales rankings&lt;/li&gt;
&lt;li&gt;Charts&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  One Final Check: Did the New Product Receive Its Ownership Tag?
&lt;/h2&gt;

&lt;p&gt;The UI worked.&lt;/p&gt;

&lt;p&gt;Still, I wanted one final check.&lt;/p&gt;

&lt;p&gt;Did the newly registered Product actually belong to the Admin Dataset?&lt;/p&gt;

&lt;p&gt;I queried PostgreSQL directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_key&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'明太フランス'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id         9
name       明太フランス
kind       admin
system_key admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;✅ The newly registered Product had the correct &lt;strong&gt;Admin Dataset ownership tag&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That verified the complete production path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Register Product
↓
Assign Admin Dataset
↓
Enter sales
↓
Save to DB
↓
Aggregate
↓
Ranking
↓
Chart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  All I Wanted Was a Guest Demo
&lt;/h2&gt;

&lt;p&gt;This entire project started with a simple idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I want people to actually try my portfolio.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the moment I said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Allow Guests to write data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the questions multiplied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How do I protect existing Admin data?

How do I isolate one Guest from another?

Who owns the existing Products?

What if the old application writes during migration?

What if NULL remains?

What if migration deletes data?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the work expanded into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dataset design
↓
DB constraints
↓
Migration
↓
Backfill
↓
NOT NULL
↓
Application changes
↓
pytest
↓
Backup
↓
Production migration
↓
DB comparison
↓
Smoke test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before creating even one Guest login button, the project had already become a fairly large construction job.&lt;/p&gt;




&lt;h2&gt;
  
  
  "It Works" Is No Longer Enough for Me
&lt;/h2&gt;

&lt;p&gt;Earlier in my learning, I probably would have thought:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;It works in the browser
↓
Done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, I kept asking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What happens if it breaks?

What happens in a partially failed state?

Did the existing data really survive?

Can the database itself reject invalid values?

Does a newly created production Product actually receive a Dataset?

Are the row counts still identical after deployment?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Continuing to strengthen pytest seems to have changed how I approach other parts of development too.&lt;/p&gt;

&lt;p&gt;I gradually started paying less attention to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Is it GREEN?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and more attention to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"What does this GREEN actually guarantee?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Stage 1 Complete
&lt;/h2&gt;

&lt;p&gt;Stage 1 was now complete:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protect the existing Admin data with Dataset boundaries.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the end of Stage 1, the structure was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL
│
└─ Admin Dataset
    │
    └─ Product
        │
        └─ DailySales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There were still no Guests.&lt;/p&gt;

&lt;p&gt;The next step at that point was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL
│
├─ Admin Dataset
│
└─ Guest Dataset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guests would need mechanisms that Admin did not, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Temporary sessions&lt;/li&gt;
&lt;li&gt;Per-Guest data isolation&lt;/li&gt;
&lt;li&gt;Protection against accessing Admin data&lt;/li&gt;
&lt;li&gt;Inactivity expiration&lt;/li&gt;
&lt;li&gt;Automatic Guest Dataset cleanup&lt;/li&gt;
&lt;li&gt;Limits on AI usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only then could I finally begin working on the Guest side of:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a portfolio people could actually interact with.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In Stage 1:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I did not create a single Guest screen.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first glance, it might look as though the Guest Demo barely progressed.&lt;/p&gt;

&lt;p&gt;But what I actually built was the foundation for:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;protecting the data that must remain safe before allowing anyone else to interact with the application.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The most important decision in this stage was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;designing protection for the existing data before adding the new feature.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The next stage would create:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a real Guest-only "room" next to the Admin Dataset.&lt;/strong&gt; 🧪&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 The Guest Demo Is Now Live
&lt;/h2&gt;

&lt;p&gt;The work described in this article was only the first stage.&lt;/p&gt;

&lt;p&gt;At that point, there were no Guests yet.&lt;/p&gt;

&lt;p&gt;Since then, the project continued through Guest identity, Dataset isolation, authorization, expiration and cleanup, usage limits, and finally the public Guest Demo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Protect existing Admin data
↓
Introduce Dataset boundaries
↓
Create Guest identity
↓
Isolate Guest data
↓
Protect application routes
↓
Add expiration and cleanup
↓
Add usage limits
↓
Open the Guest Demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original goal was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"I want people to actually try my portfolio."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is now possible.&lt;/p&gt;

&lt;p&gt;You can try &lt;code&gt;sales_data_app&lt;/code&gt; without creating an account:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bakery-salesdata.onrender.com/" rel="noopener noreferrer"&gt;https://bakery-salesdata.onrender.com/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  👤 Guest Demo Implementation Series
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stage 1 — This Article
&lt;/h3&gt;

&lt;p&gt;Migrated the existing Admin data into a Dataset boundary and prepared the database structure before adding Guest areas.&lt;/p&gt;

&lt;p&gt;Original Japanese version:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932/items/2ccaab5c1b7e29619345" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/2ccaab5c1b7e29619345&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2 — Part 1
&lt;/h3&gt;

&lt;p&gt;Protected Admin before allowing Guests in, then built the foundation for Guest identity and Dataset authorization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932/items/77cc200ab78761174b91" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/77cc200ab78761174b91&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2 — Part 2
&lt;/h3&gt;

&lt;p&gt;Safely created Guest Datasets on the server, connected them to Guest identities, and re-audited the safety conditions even after reaching 181 GREEN tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932/items/aa3b8a06029e8d5e3f25" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/aa3b8a06029e8d5e3f25&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3
&lt;/h3&gt;

&lt;p&gt;Strengthened Dataset boundaries across Product, DailySales, Dashboard, API, AI, and seed logic before allowing Guests into business routes, growing pytest from 181 to 195 tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932/items/f825aff19bff0d3d122c" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/f825aff19bff0d3d122c&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 4
&lt;/h3&gt;

&lt;p&gt;Allowed valid Guests into the real application routes and verified through real request paths that Dataset boundaries held across Products, sales, Dashboard, and AI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932/items/166113162b6a4d1a437e" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/166113162b6a4d1a437e&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 Guest Demo — Now Live
&lt;/h3&gt;

&lt;p&gt;The Guest Demo that this series was preparing for is now publicly available.&lt;/p&gt;

&lt;p&gt;From the login page, select &lt;strong&gt;"Try Guest Demo"&lt;/strong&gt; to enter without creating an account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bakery-salesdata.onrender.com/" rel="noopener noreferrer"&gt;https://bakery-salesdata.onrender.com/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/turning-pytest-into-an-incident-prevention-ledger-phase-1-from-3-simple-tests-to-9-2o53"&gt;Stage 1 — From 3 Simple Tests to 9&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-2-strengthening-sales-and-product-57b3"&gt;Stage 2 — Strengthening Sales and Product Registration&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-3-authentication-csrf-and-access-5gi2"&gt;Stage 3 — Authentication, CSRF, and Access Control&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-4-testing-failure-scenarios-71-87-12e7"&gt;Stage 4 — Testing Failure Scenarios&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-5-questioning-green-with-mutation-1e8a"&gt;Stage 5 — Questioning GREEN with Mutation Testing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Guest Demo
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://bakery-salesdata.onrender.com/" rel="noopener noreferrer"&gt;https://bakery-salesdata.onrender.com/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Qiita
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932" rel="noopener noreferrer"&gt;https://qiita.com/tosane932&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub — sales_data_app
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>postgres</category>
      <category>pytest</category>
    </item>
    <item>
      <title>Can You Explain the System AI Built for You? — I Audited My Own Web App with 20 Questions</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Sun, 30 Aug 2026 19:51:19 +0000</pubDate>
      <link>https://dev.to/tosane932/can-you-explain-the-system-ai-built-for-you-i-audited-my-own-web-app-with-20-questions-2k8o</link>
      <guid>https://dev.to/tosane932/can-you-explain-the-system-ai-built-for-you-i-audited-my-own-web-app-with-20-questions-2k8o</guid>
      <description>&lt;h2&gt;
  
  
  Hello from Japan 🇯🇵
&lt;/h2&gt;

&lt;p&gt;AI has made it possible to build web applications much faster than before.&lt;/p&gt;

&lt;p&gt;Paste in an error, and AI can help investigate the cause.&lt;/p&gt;

&lt;p&gt;Say, "I want to add a feature like this," and it can write the code.&lt;/p&gt;

&lt;p&gt;It can help create a Dockerfile, pytest tests, authentication logic, and more.&lt;/p&gt;

&lt;p&gt;It's incredibly useful.&lt;/p&gt;

&lt;p&gt;But while continuing to build my own applications, one question started bothering me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can I actually explain the system I built?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Code working correctly and understanding why it works are not the same thing.&lt;/p&gt;

&lt;p&gt;So this time, I decided to audit my own understanding of the Flask application I'm developing.&lt;/p&gt;

&lt;p&gt;The rule was simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answer 20 questions in my own words—without looking at the code, without searching, and without asking AI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only after answering would I compare my understanding with the actual implementation.&lt;/p&gt;

&lt;p&gt;The results were surprisingly interesting.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Web Application I Audited
&lt;/h2&gt;

&lt;p&gt;The application is a bakery sales-management system I've been building as a personal project.&lt;/p&gt;

&lt;p&gt;Its basic workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Register products
↓
Enter daily sales quantities
↓
Save sales data
↓
Visualize rankings and charts
↓
Send sales data to the Gemini API
↓
Display suggestions that may help with business decisions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main technologies include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python / Flask&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;SQLAlchemy&lt;/li&gt;
&lt;li&gt;Flask-Login&lt;/li&gt;
&lt;li&gt;Flask-WTF&lt;/li&gt;
&lt;li&gt;Alembic / Flask-Migrate&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;Gunicorn&lt;/li&gt;
&lt;li&gt;Render&lt;/li&gt;
&lt;li&gt;Gemini API&lt;/li&gt;
&lt;li&gt;pytest&lt;/li&gt;
&lt;li&gt;GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application works.&lt;/p&gt;

&lt;p&gt;At the time of this audit, it had 91 pytest tests.&lt;/p&gt;

&lt;p&gt;But this time, I wasn't trying to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Do I know the names of the technologies I'm using?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Can I explain why each one is there and what it actually does?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  I Defined Three Levels of Understanding
&lt;/h2&gt;

&lt;p&gt;I evaluated myself using three levels.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rating&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;I can explain it in my own words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;td&gt;I understand the general direction, but some parts are vague or misunderstood&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;I cannot explain it, or my understanding is incorrect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most important rule was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do not look at the code first.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If I looked at the implementation before answering, I could always say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It works this way because the code here says so."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But that would not tell me whether I actually understood the system beforehand.&lt;/p&gt;

&lt;p&gt;So I first answered entirely from memory.&lt;/p&gt;

&lt;p&gt;Only afterward did I inspect the real code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 20 Questions
&lt;/h2&gt;

&lt;p&gt;These were the questions I used.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Initial Rating&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Whose problem does this system solve, and what problem is it?&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Can I explain it to a non-technical store manager in 30 seconds?&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;How is it different from managing everything in Excel?&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;What remains usable if the AI becomes unavailable?&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Where does entered sales data go?&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;How does data travel from the browser to the database?&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;What is GitHub used for?&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Why does the app keep running when my own PC is turned off?&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Why am I using Docker?&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;What is the difference between Docker and Render?&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;What are the different roles of GitHub, Render, and PostgreSQL?&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;What does SQLAlchemy do?&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;What does the login system protect?&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Why does the login state remain when moving between pages?&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;What exactly am I sending to Gemini?&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;What is an API?&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;Why shouldn't an API key be stored on GitHub?&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;What are the roles of environment variables and &lt;code&gt;.gitignore&lt;/code&gt;?&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;Why did I grow pytest to 91 tests?&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;If all pytest tests are GREEN, does that mean there are no bugs?&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My initial result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟢 6
🟡 10
🔴 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The purpose was not to compete for a score.&lt;/p&gt;

&lt;p&gt;I used it as a diagnostic tool to find:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;where my own understanding stopped.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  I Could Explain the User-Facing Purpose Relatively Well
&lt;/h2&gt;

&lt;p&gt;One interesting result was that I could explain fairly well:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;why I had built the system in the first place.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example, I understood the role of AI as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It is not a system where AI makes every decision. It provides reference suggestions based on sales data so that the store manager can think about what to try next.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I could also explain that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even if the AI feature becomes unavailable, the core functions—product registration, sales entry, rankings, and charts—still remain usable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I had a reasonable understanding of:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the purpose of the system from the user's perspective.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The problems appeared behind the scenes.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Thought Sales Data Went "Through GitHub" to the Database
&lt;/h2&gt;

&lt;p&gt;One question was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How does sales data entered in the browser reach PostgreSQL?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My first explanation was something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It goes through GitHub and then reaches the database.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was wrong.&lt;/p&gt;

&lt;p&gt;The actual path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
↓
Flask application
↓
Input validation
↓
SQLAlchemy
↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub is not part of this runtime data path.&lt;/p&gt;

&lt;p&gt;I now separate the roles like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub
= Storage for source code and change history

Render
= A place where the web application runs on the internet

PostgreSQL
= A warehouse that stores product and sales data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I knew all three technology names.&lt;/p&gt;

&lt;p&gt;But I could not explain them as parts of one coherent system.&lt;/p&gt;

&lt;p&gt;That was the gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Also Thought Docker Sent My Finished Local Container Directly to Render
&lt;/h2&gt;

&lt;p&gt;I had another misunderstanding about Docker.&lt;/p&gt;

&lt;p&gt;My mental model was roughly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I build a Docker container on my PC, then send that completed container directly to Render.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But when I went through the actual Dockerfile line by line, my understanding changed.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.12-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;creates a builder stage based on Python 3.12 slim.&lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;prepares the required Python packages.&lt;/p&gt;

&lt;p&gt;After that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;creates another clean stage for production.&lt;/p&gt;

&lt;p&gt;In other words, this is a multi-stage build.&lt;/p&gt;

&lt;p&gt;I understood it using a Mini 4WD analogy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder
= Assembly pit

gcc / libpq-dev
= Tools such as cutters and files needed during assembly

requirements.txt
= Parts list

Production stage
= The actual machine that enters the race
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no need to load every assembly tool onto the race car.&lt;/p&gt;

&lt;p&gt;Only the required finished components are moved into the production stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /root/.local /root/.local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;copies the application files from the Docker build context into the production image.&lt;/p&gt;

&lt;p&gt;I now think of a Dockerfile as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;not the finished cargo itself, but a loading and assembly instruction sheet describing how the cargo should be prepared.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That mental model made much more sense to me.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Knew the Name SQLAlchemy, but I Couldn't Explain It
&lt;/h2&gt;

&lt;p&gt;SQLAlchemy was one of the biggest discoveries in this exercise.&lt;/p&gt;

&lt;p&gt;I had seen the name many times.&lt;/p&gt;

&lt;p&gt;It appears throughout my own application.&lt;/p&gt;

&lt;p&gt;But when I asked myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is SQLAlchemy?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I couldn't explain it.&lt;/p&gt;

&lt;p&gt;Looking at the real code, I found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask_sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SQLAlchemy&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SQLAlchemy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to connect SQLAlchemy with the Flask application.&lt;/p&gt;

&lt;p&gt;When saving sales data, the application uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My current mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL
= A warehouse where data is stored

SQLAlchemy
= An intermediary between Python and the database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Alchemy&lt;/code&gt; literally means "alchemy," so I decided to remember it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;a SQL alchemist that helps Python work with database data in a convenient form.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I also started thinking of these operations like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;db.session.add()
= Put something onto the shipping line as a candidate for storage

commit()
= Finalize this transaction

rollback()
= Cancel the uncommitted changes from this transaction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strictly speaking, SQL can sometimes be sent to the database before &lt;code&gt;commit()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But the transaction as a whole has not yet been committed.&lt;/p&gt;

&lt;p&gt;Before this exercise, I could list SQLAlchemy under "technologies used."&lt;/p&gt;

&lt;p&gt;I couldn't explain it this far.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Was Confusing Cookies with Browser Cache
&lt;/h2&gt;

&lt;p&gt;Authentication exposed another major misunderstanding.&lt;/p&gt;

&lt;p&gt;At first, I explained login persistence as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The browser stores the login information in its cache.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But browser cache and cookies are different things.&lt;/p&gt;

&lt;p&gt;Then I inspected the actual implementation and found another mismatch:&lt;/p&gt;

&lt;p&gt;the &lt;code&gt;main&lt;/code&gt; branch at the time of this audit was not using Google OAuth.&lt;/p&gt;

&lt;p&gt;It was using a single-administrator login system.&lt;/p&gt;

&lt;p&gt;So even my belief that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"My application uses Google login."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;was out of sync with the implementation I was actually auditing.&lt;/p&gt;

&lt;p&gt;I now think of the responsibilities like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cookie
= Entry ticket or wristband held by the browser

Session
= State such as "this user is logged in"

Flask-Login
= Staff who check that state

@login_required
= A gate saying "only authenticated users may continue"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@login_required&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;means that if an unauthenticated user directly visits &lt;code&gt;/dashboard&lt;/code&gt;, Flask-Login checks their authentication state.&lt;/p&gt;

&lt;p&gt;The implementation at the time of this audit also stored a SHA-256 fingerprint derived from &lt;code&gt;ADMIN_PASSWORD_HASH&lt;/code&gt; in the session and compared it with a fingerprint derived from the current &lt;code&gt;ADMIN_PASSWORD_HASH&lt;/code&gt; value.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A technical note about Flask sessions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Flask's default session is not necessarily a separate "box" stored on the server.&lt;/p&gt;

&lt;p&gt;By default, Flask stores session data in a signed—not encrypted—cookie on the browser side.&lt;/p&gt;

&lt;p&gt;For the purpose of understanding responsibilities, I separate the concepts like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cookie = entry ticket&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session = entry state&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This was another area where:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It works" had made me think I understood it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  I Was Mixing Up APIs and API Keys
&lt;/h2&gt;

&lt;p&gt;When I asked myself what an API was, my first answer was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An API is something like a key.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was another misunderstanding.&lt;/p&gt;

&lt;p&gt;Now I separate them like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API
= A defined interface through which different applications or services communicate

API key
= Authentication information used to access that interface

Environment variable
= A way to keep values such as API keys outside the source code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application retrieves the Gemini API key using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and prepares the Gemini client with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So this does &lt;strong&gt;not&lt;/strong&gt; mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the API key to log into the operating system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;os.environ.get()&lt;/code&gt; means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Retrieve the value named &lt;code&gt;GEMINI_API_KEY&lt;/code&gt; from the environment variables available to the running application.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;.gitignore&lt;/code&gt; Is Not a Feature That Hides Secrets
&lt;/h2&gt;

&lt;p&gt;I also had an inaccurate understanding of &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I thought of it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Something that hides variables people should not see.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not quite right.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt; is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;a set of rules telling Git which untracked files and directories it should ignore.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;.gitignore&lt;/code&gt; I inspected during this audit included entries such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;過去売上高/
*.xlsx
*.db
.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a logistics analogy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It is a "do not load" list for the truck heading to GitHub.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt; itself does not encrypt anything.&lt;/p&gt;

&lt;p&gt;And if secret information has already been committed to Git, adding the file to &lt;code&gt;.gitignore&lt;/code&gt; afterward does not remove that secret from the existing Git history.&lt;/p&gt;

&lt;p&gt;Trying to explain it forced me to understand that distinction much more clearly.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Am I Actually Sending to Gemini?
&lt;/h2&gt;

&lt;p&gt;I also inspected the AI part of the application.&lt;/p&gt;

&lt;p&gt;Initially, I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm sending the prompt written in the dashboard together with the sales numbers to Gemini.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In reality, the Gemini prompt-generation logic is separated into &lt;code&gt;prompts.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_sales_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales_summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;app.py&lt;/code&gt; creates a sales summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sales_summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;個&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked_sales&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_sales_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales_summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This inserts product names and sales quantities into the prompt.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Croissant: 80 units
White bread: 65 units
Melon bread: 42 units
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the request is sent to Gemini:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GEMINI_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the returned text is displayed on the dashboard.&lt;/p&gt;

&lt;p&gt;So the flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL
↓
Aggregate sales data in Flask
↓
Product names + sales quantities
↓
Build instructions in prompts.py
↓
Gemini API
↓
Receive suggestions
↓
Display them on the dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Saying "It Analyzes Current Social Media Trends" Was an Overstatement
&lt;/h2&gt;

&lt;p&gt;This was one of the biggest discoveries in the audit.&lt;/p&gt;

&lt;p&gt;The prompt I inspected at the time asked Gemini to consider things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recent food trends
Potential social media appeal
Patterns involving weekdays, seasons, and holidays
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because of that wording, I had started thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Gemini is actively looking up current social media and internet trends and using them in its analysis.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But when I inspected the implementation, the store-specific information sent to Gemini by this feature was basically:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;product names + aggregated sales quantities&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There was also no implementation in this flow that retrieved current web-search results or social media data and passed them to Gemini.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Writing "consider recent trends" in a prompt&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;actually searching the current web and analyzing fresh information&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;are different things.&lt;/p&gt;

&lt;p&gt;I noticed another related issue.&lt;/p&gt;

&lt;p&gt;The prompt also asked Gemini to consider:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;latent demand based on weekday, seasonal, and holiday patterns&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the store data sent to Gemini was primarily product names and aggregated sales quantities.&lt;/p&gt;

&lt;p&gt;The application was not directly sending detailed daily or weekday-level sales records to Gemini in this flow.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Asking the AI to analyze weekdays&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;giving the AI enough data to perform weekday analysis&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;are also different things.&lt;/p&gt;

&lt;p&gt;The lesson was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Writing "search for it" in a prompt
≠
Implementing search

Writing "analyze weekdays" in a prompt
≠
Providing enough weekday-level data for that analysis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was probably the clearest example in the entire audit of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;a difference between what I thought my working application could do and what the implementation actually did.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If I genuinely want future recommendations to include current web information or detailed weekday trends, I would need additional design work such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicitly integrating a web-search capability&lt;/li&gt;
&lt;li&gt;Sending daily or weekday-level sales data&lt;/li&gt;
&lt;li&gt;Defining the analysis period and conditions clearly&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Even 91 GREEN pytest Tests Do Not Mean "Safe"
&lt;/h2&gt;

&lt;p&gt;One area I could explain relatively well was pytest.&lt;/p&gt;

&lt;p&gt;At the time of this audit, the application had 91 pytest tests.&lt;/p&gt;

&lt;p&gt;Why had I grown the suite that far?&lt;/p&gt;

&lt;p&gt;Because I assumed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Users will not always operate the system exactly the way I expect them to.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending a product ID that does not exist&lt;/li&gt;
&lt;li&gt;Registering sales against a product from another month&lt;/li&gt;
&lt;li&gt;Registering sales for a discontinued product&lt;/li&gt;
&lt;li&gt;Directly accessing a protected URL without authentication&lt;/li&gt;
&lt;li&gt;Tampering with a CSRF token&lt;/li&gt;
&lt;li&gt;The Gemini API returning an error&lt;/li&gt;
&lt;li&gt;A database operation failing halfway through&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In truck driving, I cannot think about safety only in terms of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Normally, nobody would do that."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unexpected things happen.&lt;/p&gt;

&lt;p&gt;So I try to think about dangerous scenarios before they occur.&lt;/p&gt;

&lt;p&gt;I approach pytest in a similar way.&lt;/p&gt;

&lt;p&gt;I have been growing it into:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;an "incident prevention log" that records failure patterns before they can become repeat incidents.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;91 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does &lt;strong&gt;not&lt;/strong&gt; mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There are no bugs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;All 91 expectations represented by the tests at that point passed in that run.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If I discover a new blind spot, I fix it and add another regression test.&lt;/p&gt;

&lt;p&gt;I used to feel reassured simply by seeing GREEN.&lt;/p&gt;

&lt;p&gt;Now I try to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"What exactly does this GREEN result guarantee?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Not Being Able to Explain Something Wasn't Embarrassing — It Showed Me What to Learn Next
&lt;/h2&gt;

&lt;p&gt;At first, I felt a little embarrassed.&lt;/p&gt;

&lt;p&gt;This was my own application, yet I discovered that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I couldn't explain SQLAlchemy&lt;/li&gt;
&lt;li&gt;I was confusing cookies with cache&lt;/li&gt;
&lt;li&gt;I was confusing APIs with API keys&lt;/li&gt;
&lt;li&gt;I thought GitHub was part of the sales-data path&lt;/li&gt;
&lt;li&gt;I thought my local Docker container was simply sent to Render&lt;/li&gt;
&lt;li&gt;I thought the application was retrieving current social media trends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But my perspective changed during the exercise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding something I don't understand means I've found the next place to learn.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What seems more dangerous is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;continuing to use something while only thinking that I understand it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Being Able to Explain Something Simply Does Not Mean Complete Understanding
&lt;/h2&gt;

&lt;p&gt;There is an important limitation here.&lt;/p&gt;

&lt;p&gt;Being able to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL is a warehouse.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;does not mean I completely understand PostgreSQL.&lt;/p&gt;

&lt;p&gt;Calling SQLAlchemy an intermediary does not mean I understand all of its internals.&lt;/p&gt;

&lt;p&gt;What I did here was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;use explanation as a way to locate gaps in my understanding.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Being able to explain something is not the final goal.&lt;/p&gt;

&lt;p&gt;I see it as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;one diagnostic method for checking understanding.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  I Don't Need to Avoid Technical Terms
&lt;/h2&gt;

&lt;p&gt;I also don't think technical terms need to be completely removed when explaining a system.&lt;/p&gt;

&lt;p&gt;What matters is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can I translate them into words the listener can understand?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL
= A warehouse that stores application data

SQLAlchemy
= An intermediary between Python and the database

Dockerfile
= A loading and assembly instruction sheet for building the application

GitHub
= Storage for source code and change history

Render
= Rented infrastructure where the application runs

Cookie
= Entry ticket

Session
= Entry state

Flask-Login
= Entry-management staff

API
= A communication interface with another service

API key
= Authentication information used to access that interface
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combining:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the official technical term + language I personally understand&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;made the system much easier to organize in my head.&lt;/p&gt;




&lt;h2&gt;
  
  
  Writing Your Own Manual Can Be a Surprisingly Good Test
&lt;/h2&gt;

&lt;p&gt;One thing I learned from this exercise is that asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can I write the manual for my own system?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is a surprisingly powerful test.&lt;/p&gt;

&lt;p&gt;For a user-facing manual, I need to explain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What can the application do?
How do you operate it?
When is it useful?
What can it not do?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a developer-facing explanation, I need to explain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How does the data flow?
Why is this technology being used?
How does authentication work?
What is sent to the AI?
What remains available during failures?
What do the tests actually guarantee?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I get stuck while trying to write one of these explanations, that may point to a gap in my understanding.&lt;/p&gt;

&lt;p&gt;And if I find myself thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This operation is extremely difficult to explain."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;the problem may not only be my understanding.&lt;/p&gt;

&lt;p&gt;There may also be a UI/UX problem.&lt;/p&gt;

&lt;p&gt;Writing documentation can itself become a system inspection.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Is Not an Argument Against Using AI
&lt;/h2&gt;

&lt;p&gt;This article is &lt;strong&gt;not&lt;/strong&gt; saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Using AI to write code is bad.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I will continue using AI.&lt;/p&gt;

&lt;p&gt;Without AI, I probably could not have experimented this much in such a short period of time.&lt;/p&gt;

&lt;p&gt;But instead of stopping at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI created it
↓
It works
↓
Done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I think there is more value in continuing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI created it
↓
It works
↓
Why does it work?
↓
What does this component do?
↓
If I break it, will pytest detect it?
↓
Can I explain it to the user?
↓
Does my understanding match the actual code?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, AI stops being only a code-generation machine.&lt;/p&gt;

&lt;p&gt;It becomes a powerful learning tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;I audited my own web application using 20 questions.&lt;/p&gt;

&lt;p&gt;My initial result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟢 6
🟡 10
🔴 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the score itself was not important.&lt;/p&gt;

&lt;p&gt;Comparing my answers with the actual code revealed gaps such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub is not part of the runtime path for sales data&lt;/li&gt;
&lt;li&gt;A Dockerfile is an assembly instruction, not the finished product&lt;/li&gt;
&lt;li&gt;SQLAlchemy helps connect Python application logic with the database&lt;/li&gt;
&lt;li&gt;Cookies, sessions, and Flask-Login have different responsibilities&lt;/li&gt;
&lt;li&gt;An API and an API key are different things&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.gitignore&lt;/code&gt; is not an encryption feature&lt;/li&gt;
&lt;li&gt;I verified what data was actually being sent to Gemini&lt;/li&gt;
&lt;li&gt;My belief that the application was analyzing current social media trends did not match the implementation&lt;/li&gt;
&lt;li&gt;What I ask an AI to analyze is not necessarily the same as the data I actually provide for that analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest lesson was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What I thought I knew was more dangerous than what I knew I didn't know.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the AI era, the speed at which we can generate code will probably continue to increase.&lt;/p&gt;

&lt;p&gt;That may make it even more important to spend time asking, in our own words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is this here for?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;How does it work?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;What happens if it breaks?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Can I explain it to the user?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I want to continue building with one rule in mind:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't stop at "it works." Keep going until I can explain it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/turning-pytest-into-an-incident-prevention-ledger-phase-1-from-3-simple-tests-to-9-2o53"&gt;Stage 1 — From 3 Simple Tests to 9&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-2-strengthening-sales-and-product-57b3"&gt;Stage 2 — Strengthening Sales and Product Registration&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-3-authentication-csrf-and-access-5gi2"&gt;Stage 3 — Authentication, CSRF, and Access Control&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-4-testing-failure-scenarios-71-87-12e7"&gt;Stage 4 — Testing Failure Scenarios&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-5-questioning-green-with-mutation-1e8a"&gt;Stage 5 — Questioning GREEN with Mutation Testing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>ai</category>
      <category>pytest</category>
    </item>
    <item>
      <title>📝 Growing pytest into an “Incident Prevention Log” — Stage 5: Questioning GREEN with Mutation Testing</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Thu, 27 Aug 2026 23:03:25 +0000</pubDate>
      <link>https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-5-questioning-green-with-mutation-1e8a</link>
      <guid>https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-5-questioning-green-with-mutation-1e8a</guid>
      <description>&lt;h2&gt;
  
  
  Hello from Japan 🇯🇵
&lt;/h2&gt;

&lt;p&gt;I'm a truck driver in Japan, teaching myself web application development mainly with Python while continuing to work full-time.&lt;/p&gt;

&lt;p&gt;In my personal Flask application, I've been gradually growing pytest into more than just a tool for checking whether the application works.&lt;/p&gt;

&lt;p&gt;I think of it as an &lt;strong&gt;"incident prevention log"&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;a regression suite that helps stop previously discovered failures and near misses from silently returning.&lt;/p&gt;

&lt;p&gt;This is Stage 5, the final stage of that series.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  87 GREEN Tests Still Had Five Blind Spots
&lt;/h2&gt;

&lt;p&gt;The pytest suite originally contained only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, I strengthened it step by step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stage 1:  3 → 9
Stage 2:  9 → 51
Stage 3: 51 → 69
Additional demo seed tests: 69 → 71
Stage 4: 71 → 87
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stage 5 took a slightly different direction.&lt;/p&gt;

&lt;p&gt;Up through Stages 1–4, the process had mainly been:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;There might be a problem
↓
Write a pytest test
↓
RED
↓
Fix the problem
↓
GREEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But once all 87 tests were GREEN, one question came to mind:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I really trust that GREEN?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the tests themselves have blind spots, broken code may still remain GREEN.&lt;/p&gt;

&lt;p&gt;So in Stage 5, I decided to do the opposite:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;temporarily break working code on purpose and check whether pytest actually turns RED.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This time, I borrowed the idea of &lt;strong&gt;falsification&lt;/strong&gt;: instead of assuming that GREEN meant the tests were strong, I tried to disprove that assumption with manual mutation testing.&lt;/p&gt;

&lt;p&gt;The result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starting point: 87 passed

Mutations performed: 11

Initially KILLED:   6
Initially SURVIVED: 5

Strengthen pytest for the 5 survivors
↓
Apply the same mutations again
↓
Confirm all 5 now turn RED

Final result:
91 passed, 2 known warnings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So even with &lt;strong&gt;87 GREEN tests, five kinds of failures could still slip through.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article focuses mainly on those five survivors.&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Stage 1&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/f3de1e190873a90de39f" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/f3de1e190873a90de39f&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 2&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/b91261e7103df5792f7d" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/b91261e7103df5792f7d&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 3&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/6d1ca5490979c8cf9d62" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/6d1ca5490979c8cf9d62&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 4&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/372270330e73583a227f" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/372270330e73583a227f&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 5&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/85fd24c7baa6fe7c76a7" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/85fd24c7baa6fe7c76a7&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  In Stage 5, I Started Questioning GREEN
&lt;/h2&gt;

&lt;p&gt;When pytest is GREEN, it means:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the current implementation satisfied the conditions expressed by the tests that currently exist.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GREEN = completely safe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, imagine this safe JavaScript code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose pytest only checks that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;textContent is used
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then someone adds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;directly after it.&lt;/p&gt;

&lt;p&gt;The original &lt;code&gt;textContent&lt;/code&gt; is still present.&lt;/p&gt;

&lt;p&gt;So the test may still pass even though a dangerous HTML sink has been introduced.&lt;/p&gt;

&lt;p&gt;In other words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Safe code exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dangerous code does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are two different things.&lt;/p&gt;

&lt;p&gt;Instead of only imagining this possibility, I decided to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;actually break the code and see what happened.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Mutation Testing?
&lt;/h2&gt;

&lt;p&gt;Mutation testing intentionally introduces small defects into otherwise working code.&lt;/p&gt;

&lt;p&gt;For example, change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or take this query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DailySales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and remove:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sale_date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run pytest against the mutated code.&lt;/p&gt;

&lt;p&gt;The result generally falls into two categories.&lt;/p&gt;

&lt;h3&gt;
  
  
  KILLED
&lt;/h3&gt;

&lt;p&gt;A mutation is &lt;strong&gt;KILLED&lt;/strong&gt; when introducing the defect causes pytest to turn RED.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal code
↓
GREEN

Mutation
↓
RED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the test suite can detect that failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  SURVIVED
&lt;/h3&gt;

&lt;p&gt;A mutation &lt;strong&gt;SURVIVES&lt;/strong&gt; when pytest remains GREEN even after the defect is introduced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal code
↓
GREEN

Mutation
↓
GREEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That suggests the tests may not be able to distinguish:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the correct implementation from the broken implementation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I did not introduce a dedicated mutation testing framework in this stage.&lt;/p&gt;

&lt;p&gt;Instead, Codex and I selected representative defects and manually applied them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;one mutation at a time.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Because I Was Intentionally Breaking Working Code, I Defined Safety Rules First
&lt;/h2&gt;

&lt;p&gt;Mutation testing means intentionally putting production code into a broken state.&lt;/p&gt;

&lt;p&gt;So before starting, I defined strict boundaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not mutate main

Work only on feature/pytest-stage5

Only one mutation at a time

Do not connect to the production database

Do not connect to the development database

Do not connect to Render

Do not call the real Gemini API

Do not access external networks

Use only isolated pytest SQLite / temporary databases

Do not commit mutations

Do not push mutations

Always restore the original code after verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a mutation SURVIVED, I also did &lt;strong&gt;not&lt;/strong&gt; start modifying pytest while the broken production code was still in place.&lt;/p&gt;

&lt;p&gt;Every survivor followed this process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apply mutation
↓
Run pytest
↓
Confirm SURVIVED
↓
Restore normal code
↓
Add or strengthen pytest formally
↓
Confirm GREEN with normal code
↓
Apply the same mutation again
↓
Confirm RED
↓
Restore again
↓
Confirm GREEN with normal code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All five surviving mutations followed this same sequence.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 11 Mutations
&lt;/h2&gt;

&lt;p&gt;These are the 11 mutations I selected.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Mutation&lt;/th&gt;
&lt;th&gt;Initial Result&lt;/th&gt;
&lt;th&gt;After Strengthening&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;innerHTML&lt;/code&gt; next to XSS-safe &lt;code&gt;textContent&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;SURVIVED&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Disable CSRF protection&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Remove session fingerprint mismatch rejection&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Change year/month validation from &lt;code&gt;or&lt;/code&gt; to &lt;code&gt;and&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Disable discontinued-product validation&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Change the model &lt;code&gt;UniqueConstraint&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Change the migration &lt;code&gt;UniqueConstraint&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Remove the &lt;code&gt;year&lt;/code&gt; condition from AI advice data retrieval&lt;/td&gt;
&lt;td&gt;SURVIVED&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Add the Jinja &lt;code&gt;safe&lt;/code&gt; filter to the initial ranking&lt;/td&gt;
&lt;td&gt;SURVIVED&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Treat a session with a missing fingerprint as valid&lt;/td&gt;
&lt;td&gt;SURVIVED&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Remove the &lt;code&gt;date&lt;/code&gt; condition from sales lookup&lt;/td&gt;
&lt;td&gt;SURVIVED&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first run produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KILLED   6
SURVIVED 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The six mutations that were KILLED immediately were already detected by existing pytest coverage, so I did not add new test functions for them.&lt;/p&gt;

&lt;p&gt;The rest of this article focuses mainly on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the five mutations that survived.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Six Mutations Were KILLED Immediately
&lt;/h2&gt;

&lt;p&gt;The six mutations detected by the existing suite were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;#2 Disable CSRF protection&lt;/li&gt;
&lt;li&gt;#3 Remove session fingerprint mismatch rejection&lt;/li&gt;
&lt;li&gt;#4 Break year/month validation&lt;/li&gt;
&lt;li&gt;#5 Disable discontinued-product validation&lt;/li&gt;
&lt;li&gt;#6 Break the model &lt;code&gt;UniqueConstraint&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;#7 Break the migration &lt;code&gt;UniqueConstraint&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, in Mutation #2, I temporarily disabled CSRF protection.&lt;/p&gt;

&lt;p&gt;Requests that should have been rejected with HTTP 400 were then able to reach application logic, and the existing tests turned RED.&lt;/p&gt;

&lt;p&gt;For Mutation #5, I changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allowed discontinued products to pass through sales processing.&lt;/p&gt;

&lt;p&gt;The existing pytest suite detected that too.&lt;/p&gt;

&lt;p&gt;Mutation #7 was especially interesting.&lt;/p&gt;

&lt;p&gt;The migration originally had a unique constraint on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product_id + date
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I temporarily changed that to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product_id + quantity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration itself still completed successfully.&lt;/p&gt;

&lt;p&gt;But the pytest test that inspected the resulting schema detected that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the required UniqueConstraint does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and turned RED.&lt;/p&gt;

&lt;p&gt;That confirmed another distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The migration completed successfully" and "the correct schema was created" are not the same thing.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  SURVIVED #1: A Safe &lt;code&gt;textContent&lt;/code&gt; Check Missed a Dangerous &lt;code&gt;innerHTML&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The first surviving mutation involved XSS protection.&lt;/p&gt;

&lt;p&gt;The dynamic ranking displays product names using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I temporarily added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;immediately after it.&lt;/p&gt;

&lt;p&gt;So the mutated code looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The safe &lt;code&gt;textContent&lt;/code&gt; line still existed.&lt;/p&gt;

&lt;p&gt;But now the unsafe &lt;code&gt;innerHTML&lt;/code&gt; assignment existed too.&lt;/p&gt;

&lt;p&gt;I ran the existing XSS regression tests.&lt;/p&gt;

&lt;p&gt;The result was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GREEN.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mutation SURVIVED.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Did It Survive?
&lt;/h3&gt;

&lt;p&gt;The existing pytest test checked that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;textContent is used
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it did not check that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;innerHTML is not used
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test only proved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Safe behavior exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It did not prove:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dangerous behavior does not also exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I strengthened the regression tests to also detect dangerous HTML sinks that could become XSS vectors when used with untrusted data, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;innerHTML&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;outerHTML&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insertAdjacentHTML&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After confirming GREEN with the normal implementation, I applied the exact same mutation again.&lt;/p&gt;

&lt;p&gt;This time, pytest turned RED.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SURVIVED
↓
Strengthen assertion
↓
Apply same mutation again
↓
KILLED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test count became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;87 → 88 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first lesson was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sometimes it's not enough to confirm that something safe exists. You also need to confirm that something dangerous does not exist.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Second Wave Focused Only on Failures Existing pytest Might Miss
&lt;/h2&gt;

&lt;p&gt;You can create almost unlimited mutations if you want to.&lt;/p&gt;

&lt;p&gt;Change a condition.&lt;/p&gt;

&lt;p&gt;Remove a return.&lt;/p&gt;

&lt;p&gt;Disable exception handling.&lt;/p&gt;

&lt;p&gt;Remove a database filter.&lt;/p&gt;

&lt;p&gt;Add unsafe template behavior.&lt;/p&gt;

&lt;p&gt;But the purpose of Stage 5 was &lt;strong&gt;not&lt;/strong&gt; to maximize the number of mutations.&lt;/p&gt;

&lt;p&gt;After the first wave, I inspected the code again without changing production code.&lt;/p&gt;

&lt;p&gt;I selected four additional mutations where I thought:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The current fixture might not distinguish
correct code from broken code

The current assertion might miss this failure

This mutation might reveal something different
from the existing tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those became Mutations #8 through #11.&lt;/p&gt;

&lt;p&gt;I decided these would be the final group for Stage 5.&lt;/p&gt;

&lt;p&gt;The result:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;all four SURVIVED.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This became the most important part of Stage 5 for me.&lt;/p&gt;




&lt;h2&gt;
  
  
  SURVIVED #8: Removing the &lt;code&gt;year&lt;/code&gt; Filter Changed Nothing Because the Fixture Was Too Simple
&lt;/h2&gt;

&lt;p&gt;The AI business-advice feature retrieves sales data for a selected year and month.&lt;/p&gt;

&lt;p&gt;The normal code calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;_get_sales_from_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I temporarily removed the year condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;_get_sales_from_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So instead of asking for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;August 2026
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the mutated code effectively asked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;August from any year
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran the existing pytest test.&lt;/p&gt;

&lt;p&gt;The result:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GREEN.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mutation SURVIVED.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem Was the Fixture, Not the Production Code
&lt;/h3&gt;

&lt;p&gt;At the time, the fixture only contained data for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;August 2026
July 2026
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So even after removing the &lt;code&gt;year&lt;/code&gt; condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;month = August
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;still returned only the August 2026 data.&lt;/p&gt;

&lt;p&gt;The correct code and the mutated code produced:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the same result with the existing fixture.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;August 2025
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;data to the fixture.&lt;/p&gt;

&lt;p&gt;The product name was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Previous-year August product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with quantity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;77
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, when requesting August 2026, I strengthened the existing test with an assertion equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Previous-year August product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;contents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normal code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GREEN.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same mutation applied again:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RED.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mutation was now KILLED.&lt;/p&gt;

&lt;p&gt;Because I strengthened an existing test function instead of adding a new one, the total remained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;88 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What I Learned Here
&lt;/h3&gt;

&lt;p&gt;Test data does not merely need to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;exist.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It needs enough discriminating power to produce different results between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;correct code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;broken code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test can execute a line of code without proving:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;that the condition on that line is actually necessary.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  SURVIVED #9: Existing XSS Tests Missed Jinja &lt;code&gt;|safe&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The next mutation involved the dashboard's initial server-rendered ranking.&lt;/p&gt;

&lt;p&gt;Normally, Jinja autoescapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ name }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I temporarily changed it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ name | safe }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I ran the existing XSS tests.&lt;/p&gt;

&lt;p&gt;The result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mutation SURVIVED.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Did It Survive Even Though I Already Had XSS Tests?
&lt;/h3&gt;

&lt;p&gt;The existing tests focused mostly on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rankings generated later by JavaScript&lt;/li&gt;
&lt;li&gt;AI responses&lt;/li&gt;
&lt;li&gt;unsafe HTML sinks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But they did not directly verify:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the product name rendered by the server in the initial ranking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I inserted a product name like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;em&amp;gt;HTML-like product name&amp;lt;/em&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into the database.&lt;/p&gt;

&lt;p&gt;If normal Jinja autoescaping is working, &lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt; should appear as text rather than becoming an actual HTML element.&lt;/p&gt;

&lt;p&gt;The new pytest test verifies that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;escaped text exists in the raw HTML&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.prod-name&lt;/code&gt; displays the original product-name text&lt;/li&gt;
&lt;li&gt;no &lt;code&gt;em&lt;/code&gt; element appears inside &lt;code&gt;.prod-name&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Normal implementation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GREEN.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reapply the &lt;code&gt;|safe&lt;/code&gt; mutation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RED.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SURVIVED
↓
Add server-rendered XSS coverage
↓
KILLED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test count became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;88 → 89 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This taught me that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;having something called an "XSS test" does not mean every rendering path is protected by that test.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  SURVIVED #10: A Normal Login Fixture Could Never Produce a Missing-Fingerprint Session
&lt;/h2&gt;

&lt;p&gt;The existing session tests verified whether an old session was rejected after administrator credentials changed.&lt;/p&gt;

&lt;p&gt;But what happens if:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the fingerprint itself disappears from the session?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The normal implementation rejects a session if the fingerprint is missing.&lt;/p&gt;

&lt;p&gt;So I introduced a mutation that treated a missing fingerprint as though it matched the current fingerprint.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fingerprint missing
↓
Pretend it matches the current fingerprint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The existing pytest tests returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mutation SURVIVED.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem Was the "Normal Login" Fixture
&lt;/h3&gt;

&lt;p&gt;The existing tests logged in normally.&lt;/p&gt;

&lt;p&gt;A successful login naturally stores the fingerprint in the session.&lt;/p&gt;

&lt;p&gt;So those tests never created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a session where only the fingerprint is missing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the new test, I performed a normal login and then used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;session_transaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to remove only the fingerprint from the session.&lt;/p&gt;

&lt;p&gt;The Flask-Login &lt;code&gt;_user_id&lt;/code&gt; remained.&lt;/p&gt;

&lt;p&gt;Now the behavior differed:&lt;/p&gt;

&lt;p&gt;Normal implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;redirect to login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mutated implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dashboard → 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I confirmed GREEN with the normal code.&lt;/p&gt;

&lt;p&gt;Then I applied the mutation again.&lt;/p&gt;

&lt;p&gt;pytest turned RED.&lt;/p&gt;

&lt;p&gt;KILLED.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;89 → 90 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This showed me that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;fixtures created only through normal user flows may be unable to reproduce abnormal states.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  SURVIVED #11: Removing the &lt;code&gt;date&lt;/code&gt; Filter Changed Nothing Because There Was No Same-Product, Different-Date Data
&lt;/h2&gt;

&lt;p&gt;This was the final mutation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DailySales&lt;/code&gt; searches for the row to update with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DailySales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product
+
date
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;must match.&lt;/p&gt;

&lt;p&gt;I temporarily removed the &lt;code&gt;date&lt;/code&gt; condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DailySales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I ran the existing normal sales POST tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PASS.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I also ran the database &lt;code&gt;UniqueConstraint&lt;/code&gt; tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PASS.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mutation SURVIVED.&lt;/p&gt;

&lt;h3&gt;
  
  
  Again, the Fixture Was the Problem
&lt;/h3&gt;

&lt;p&gt;The existing fixture did not contain:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sales for the same product on another date.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a product only has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;August 2 sale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then removing the date filter may still return that exact same row by coincidence.&lt;/p&gt;

&lt;p&gt;So I created an existing sales record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-08-01
quantity=5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I POSTed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-08-02
quantity=9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the normal implementation, the expected result is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;August 1 → quantity=5
August 2 → quantity=9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the mutated implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;August 1 → quantity=9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the historical August 1 sale to be incorrectly changed from 5 to 9&lt;/li&gt;
&lt;li&gt;the new August 2 sale to never be created&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the new pytest test verifies both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The August 1 record remains unchanged

and

The August 2 record is created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normal implementation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GREEN.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same mutation reapplied:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RED.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;KILLED.&lt;/p&gt;

&lt;p&gt;The total became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;90 → 91 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  All Four Mutations in the Second Wave Initially SURVIVED
&lt;/h2&gt;

&lt;p&gt;The second-wave results were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mutation&lt;/th&gt;
&lt;th&gt;Initial Result&lt;/th&gt;
&lt;th&gt;Why It Survived&lt;/th&gt;
&lt;th&gt;After Strengthening&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;#8 Remove &lt;code&gt;year&lt;/code&gt; condition&lt;/td&gt;
&lt;td&gt;SURVIVED&lt;/td&gt;
&lt;td&gt;No previous-year same-month fixture&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#9 Add Jinja `\&lt;/td&gt;
&lt;td&gt;safe`&lt;/td&gt;
&lt;td&gt;SURVIVED&lt;/td&gt;
&lt;td&gt;Initial server rendering wasn't directly tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#10 Missing fingerprint&lt;/td&gt;
&lt;td&gt;SURVIVED&lt;/td&gt;
&lt;td&gt;No abnormal-session fixture&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#11 Remove &lt;code&gt;date&lt;/code&gt; condition&lt;/td&gt;
&lt;td&gt;SURVIVED&lt;/td&gt;
&lt;td&gt;No same-product, different-date sales data&lt;/td&gt;
&lt;td&gt;KILLED&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This really stayed with me.&lt;/p&gt;

&lt;p&gt;By the end of Stage 4, the suite had already grown to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;87 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yet all four mutations I deliberately selected because:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This might still be weak"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;passed straight through the suite.&lt;/p&gt;

&lt;p&gt;More tests can create a greater sense of confidence.&lt;/p&gt;

&lt;p&gt;But:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;There are 87 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Those 87 tests can distinguish important failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are not the same statement.&lt;/p&gt;




&lt;h2&gt;
  
  
  In the End, All 11 Selected Mutations Became Killable
&lt;/h2&gt;

&lt;p&gt;I performed 11 mutations.&lt;/p&gt;

&lt;p&gt;The first run produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KILLED   6
SURVIVED 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The five survivors were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#1
#8
#9
#10
#11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each of them, I followed the full process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Restore mutation
↓
Formally add or strengthen pytest
↓
Confirm GREEN with normal code
↓
Apply the same mutation again
↓
Confirm RED
↓
Restore again
↓
Confirm final GREEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So for the &lt;strong&gt;11 representative mutations I selected&lt;/strong&gt;, pytest was ultimately able to KILL all of them.&lt;/p&gt;

&lt;p&gt;But I want to be careful about what that means.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mutation Score for the whole application = 100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I only mutated:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11 representative cases that I selected manually.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Grew from 87 to 91 Tests
&lt;/h2&gt;

&lt;p&gt;At the beginning of Stage 5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;87 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;91 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only four new test functions were added.&lt;/p&gt;

&lt;p&gt;Since I performed 11 mutations, I initially thought the suite might grow more.&lt;/p&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;six mutations were already KILLED by existing tests&lt;/li&gt;
&lt;li&gt;#8 only required strengthening the fixture in an existing test function&lt;/li&gt;
&lt;li&gt;new test functions were added for #1, #9, #10, and #11&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;87 + 4 = 91
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I actually think that was a good result.&lt;/p&gt;

&lt;p&gt;If my goal had been:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reach 100 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I might have added tests simply to make the number larger.&lt;/p&gt;

&lt;p&gt;But the purpose of Stage 5 was not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;to increase the test count.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;to evaluate the failure-detection ability of the GREEN tests I already had.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  From the Original Three Tests to About 30 Times as Many
&lt;/h2&gt;

&lt;p&gt;Before I started strengthening pytest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stage 1    9
Stage 2   51
Stage 3   69
Demo seed 71
Stage 4   87
Stage 5   91
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Numerically, that is roughly 30 times the original count.&lt;/p&gt;

&lt;p&gt;But after Stage 5, what matters more to me is not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;that I have 91 tests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;that I can explain which failures cause those tests to turn RED.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 5 Made No Formal Production-Code Changes
&lt;/h2&gt;

&lt;p&gt;The only files with formal changes from Stage 5 were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_ai_integration.py
test_auth.py
test_sales.py
test_xss_regressions.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There were no formal production-code changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.py        no formal diff
templates     no formal diff
models        no formal diff
migrations    no formal diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every temporary mutation was restored.&lt;/p&gt;

&lt;p&gt;So Stage 5 did not strengthen the application by permanently changing production code.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;it strengthened pytest's ability to detect defects.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Five Things I Learned from Stage 5
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. GREEN Can Still Miss Broken Code
&lt;/h3&gt;

&lt;p&gt;Five mutations actually passed through the suite while it remained GREEN.&lt;/p&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest is GREEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not guarantee:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No broken code exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GREEN means:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the current tests did not detect a violation of the conditions they express.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Fixtures Need Discriminating Power
&lt;/h3&gt;

&lt;p&gt;Mutation #8 could not be detected without data from the same month in another year.&lt;/p&gt;

&lt;p&gt;Mutation #11 could not be detected without sales for the same product on another date.&lt;/p&gt;

&lt;p&gt;So a fixture should not merely be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data that lets the test run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It may need to be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data that distinguishes correct behavior
from broken behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. Assertions Sometimes Need to Check Both "Exists" and "Does Not Exist"
&lt;/h3&gt;

&lt;p&gt;Mutation #1 showed this clearly.&lt;/p&gt;

&lt;p&gt;I already verified that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;textContent is used
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that assertion still passed after dangerous &lt;code&gt;innerHTML&lt;/code&gt; was added next to it.&lt;/p&gt;

&lt;p&gt;So I needed to test both directions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Safe behavior exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dangerous behavior does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4. Normal Fixtures Cannot Always Represent Abnormal States
&lt;/h3&gt;

&lt;p&gt;Mutation #10 involved a session where only the fingerprint was missing.&lt;/p&gt;

&lt;p&gt;A normal login always creates that fingerprint.&lt;/p&gt;

&lt;p&gt;So I had to explicitly construct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;an abnormal session state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normal user-flow fixtures alone do not always reach the failure condition you actually need to test.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. RED Does Not Automatically Mean the Mutation Was KILLED
&lt;/h3&gt;

&lt;p&gt;During mutation testing, I did not classify something as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KILLED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;just because pytest became RED.&lt;/p&gt;

&lt;p&gt;I checked whether:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the intended mutation caused the intended assertion to fail.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the test failed for an unrelated reason, that would not prove that the mutation itself had been detected.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Scariest Part Wasn't Breaking the Code — It Was Accidentally Leaving It Broken
&lt;/h2&gt;

&lt;p&gt;The part I was most careful about was not the mutation itself.&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;accidentally leaving intentionally broken code behind.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So every mutation followed the rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not commit
Do not push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And every cycle included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mutation
↓
pytest
↓
restore
↓
git diff
↓
pytest with normal code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mutation testing is often described as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"break it and see if the tests catch it."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But for me, reliably restoring the code is part of the same operation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Result
&lt;/h2&gt;

&lt;p&gt;The final pytest result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;91 passed, 2 warnings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two warnings are the previously known:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flask-SQLAlchemy get_engine() DeprecationWarning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;migrations/env.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They were not introduced by Stage 5.&lt;/p&gt;

&lt;p&gt;I'm treating them as separate maintenance work, so I did not fix them as part of this stage.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Still Does Not Mean the Application Is Completely Safe
&lt;/h2&gt;

&lt;p&gt;I performed 11 mutations and eventually strengthened the suite so that all 11 selected failures could be detected.&lt;/p&gt;

&lt;p&gt;But I still cannot say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This application is completely safe.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are many areas outside the scope of this work, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL-specific behavior&lt;/li&gt;
&lt;li&gt;locking&lt;/li&gt;
&lt;li&gt;concurrency&lt;/li&gt;
&lt;li&gt;race conditions&lt;/li&gt;
&lt;li&gt;simultaneous POST requests&lt;/li&gt;
&lt;li&gt;duplicate submissions&lt;/li&gt;
&lt;li&gt;differences from the Render environment&lt;/li&gt;
&lt;li&gt;production environment variables&lt;/li&gt;
&lt;li&gt;the real Gemini API&lt;/li&gt;
&lt;li&gt;API quotas&lt;/li&gt;
&lt;li&gt;timeouts&lt;/li&gt;
&lt;li&gt;SDK changes&lt;/li&gt;
&lt;li&gt;real-browser DOM behavior&lt;/li&gt;
&lt;li&gt;CSP&lt;/li&gt;
&lt;li&gt;specifications I have not decided yet&lt;/li&gt;
&lt;li&gt;future code&lt;/li&gt;
&lt;li&gt;failures I have not imagined yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And I did not mutate the entire application exhaustively.&lt;/p&gt;

&lt;p&gt;What I verified was narrower:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can pytest actually detect these 11 representative types of failure that I selected?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  It Feels a Little Like Testing a Truck's Safety Equipment
&lt;/h2&gt;

&lt;p&gt;I work as a truck driver.&lt;/p&gt;

&lt;p&gt;A truck has many safety systems.&lt;/p&gt;

&lt;p&gt;But simply saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The safety equipment exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not mean accidents can never happen.&lt;/p&gt;

&lt;p&gt;When a dangerous situation occurs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;does that equipment actually work the way it is supposed to?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;am I becoming overconfident simply because the equipment exists?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I started seeing something similar with pytest.&lt;/p&gt;

&lt;p&gt;It is not enough to say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest exists
CI exists
authentication exists
CSRF protection exists
database constraints exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also need to ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;if one of those things breaks, can the system actually detect that something is wrong?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And even excellent safety equipment does not automatically make every human decision safe.&lt;/p&gt;

&lt;p&gt;Years of experience do not guarantee perfect judgment either.&lt;/p&gt;

&lt;p&gt;Familiarity can sometimes lead to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This is probably fine.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Software may have a similar trap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;All pytest tests are GREEN,
so it's probably fine.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I stop thinking there, I may miss something important.&lt;/p&gt;




&lt;h2&gt;
  
  
  Not "It's Probably Fine," but "What If?"
&lt;/h2&gt;

&lt;p&gt;In Stage 5, I used a mindset from my day job almost directly.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It's probably fine."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I try to think:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What if?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest is GREEN,
but it might still miss a failure

Dangerous behavior might be added
next to safe behavior

The fixture might be too simple
to distinguish broken code
from correct code

A migration might succeed
while producing the wrong schema

I might only be testing normal login
and never checking an abnormal session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I thought through those possibilities and then:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;introduced small defects to test them for real.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was Stage 5.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Stage 5 moved the suite from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;87 passed
↓
91 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the important part was not the additional four tests.&lt;/p&gt;

&lt;p&gt;I introduced 11 representative defects.&lt;/p&gt;

&lt;p&gt;The first run produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initially KILLED   6
Initially SURVIVED 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Investigating the five survivors showed that the problem was not always simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Not enough tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blind spots included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Assertions that only checked one direction

Fixtures without enough comparison data

Server-rendered output not being tested

No abnormal-session fixture

No different-date sales data for the same product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For all five survivors, I completed this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Restore normal code
↓
Add or strengthen pytest
↓
GREEN with normal code
↓
Apply the same mutation again
↓
RED
↓
Restore again
↓
Final GREEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in the end:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pytest could KILL all 11 mutations selected for this stage.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Stage 1 began with only three pytest tests and the idea of turning them into a regression suite.&lt;/p&gt;

&lt;p&gt;Stage 2 expanded into validation, database updates, and rollback behavior.&lt;/p&gt;

&lt;p&gt;Stage 3 expanded into authentication, CSRF protection, and access control.&lt;/p&gt;

&lt;p&gt;Stage 4 looked ahead to failures that had not happened yet, including migrations, AI-service failures, and session problems.&lt;/p&gt;

&lt;p&gt;And Stage 5:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;questioned the GREEN result itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The biggest lesson for me was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GREEN ≠ proof of safety
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GREEN means:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the current implementation satisfied the conditions expressed by the current tests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is why I think there is value in occasionally turning the question around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can this test actually detect the failure
it is supposed to detect?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pytest started with three tests.&lt;/p&gt;

&lt;p&gt;It ended this series with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;91 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But now I care more about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;being able to explain what I can break to make those tests turn RED&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;than simply saying:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I have 91 tests."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For me, this marks the end of Stage 5.&lt;/p&gt;

&lt;p&gt;What began as a small list of normal behavior checks gradually became an:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"incident prevention log" that I also crash-tested to see whether it could actually detect failures.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/turning-pytest-into-an-incident-prevention-ledger-phase-1-from-3-simple-tests-to-9-2o53"&gt;Stage 1 — From 3 Simple Tests to 9&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-2-strengthening-sales-and-product-57b3"&gt;Stage 2 — Strengthening Sales and Product Registration&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-3-authentication-csrf-and-access-5gi2"&gt;Stage 3 — Authentication, CSRF, and Access Control&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-4-testing-failure-scenarios-71-87-12e7"&gt;Stage 4 — Testing Failure Scenarios&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-5-questioning-green-with-mutation-1e8a"&gt;Stage 5 — Questioning GREEN with Mutation Testing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932" rel="noopener noreferrer"&gt;https://qiita.com/tosane932&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>pytest</category>
      <category>testing</category>
      <category>mutationtesting</category>
    </item>
    <item>
      <title>📝 Growing pytest into an “Incident Prevention Log” — Stage 4: Testing Failure Scenarios (71 ➡ 87 Tests)</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Sun, 23 Aug 2026 09:26:35 +0000</pubDate>
      <link>https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-4-testing-failure-scenarios-71-87-12e7</link>
      <guid>https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-4-testing-failure-scenarios-71-87-12e7</guid>
      <description>&lt;h2&gt;
  
  
  Hello from Japan 🇯🇵
&lt;/h2&gt;

&lt;p&gt;I'm a truck driver in Japan, teaching myself web application development mainly with Python while continuing to work full-time.&lt;/p&gt;

&lt;p&gt;This article is Stage 4 of my ongoing effort to strengthen pytest in a Flask application I'm building.&lt;/p&gt;

&lt;p&gt;Instead of only adding more tests for normal behavior, Stage 4 focuses on questions like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What if the input is invalid?"&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;"What if the external AI service goes down?"&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;"What if authentication settings change after login?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is similar to the kind of hazard anticipation I use in my day job as a truck driver: thinking about what &lt;strong&gt;might&lt;/strong&gt; happen before it becomes an accident.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I used Codex to help with investigation and implementation.&lt;/p&gt;

&lt;p&gt;Some of the internal behavior became complicated enough that there were points where my own understanding did not fully keep up.&lt;/p&gt;

&lt;p&gt;So rather than pretending to explain internal details I don't completely understand, this article focuses on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;what I considered a problem, what I checked, and what the result was.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Stage 1&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/f3de1e190873a90de39f" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/f3de1e190873a90de39f&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 2&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/b91261e7103df5792f7d" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/b91261e7103df5792f7d&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 3&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/6d1ca5490979c8cf9d62" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/6d1ca5490979c8cf9d62&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 4&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/372270330e73583a227f" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/372270330e73583a227f&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 5&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/85fd24c7baa6fe7c76a7" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/85fd24c7baa6fe7c76a7&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I've been gradually strengthening pytest in my personal Flask application.&lt;/p&gt;

&lt;p&gt;The test suite originally had only three tests.&lt;/p&gt;

&lt;p&gt;It grew through the previous stages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stage 1: 3 → 9 tests&lt;/li&gt;
&lt;li&gt;Stage 2: 9 → 51 tests&lt;/li&gt;
&lt;li&gt;Stage 3: 51 → 69 tests&lt;/li&gt;
&lt;li&gt;additional demo seed tests: 69 → 71 tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So at the beginning of Stage 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;71 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By this point, I wanted to check more than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Does the application work correctly under normal conditions?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I also wanted to ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"When something abnormal happens, does the application fail safely?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my work as a truck driver, I constantly think about things that might happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;someone might suddenly step or drive out&lt;/li&gt;
&lt;li&gt;cargo might shift or collapse&lt;/li&gt;
&lt;li&gt;a delivery mistake might happen&lt;/li&gt;
&lt;li&gt;a safety device might not work correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The idea is to anticipate danger before an accident occurs.&lt;/p&gt;

&lt;p&gt;I approached Stage 4 of pytest in much the same way.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Fix the accident after it happens."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wanted to move toward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Find the places where an accident might happen before it does."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That became the theme of Stage 4.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 4 Results
&lt;/h2&gt;

&lt;p&gt;By the end of this stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;71 tests
↓
87 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added &lt;strong&gt;16 tests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breakdown of the 16 New Tests&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Added&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Empty-database migration&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-integer dashboard query parameters&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini failure fallbacks&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authenticated AI advice happy path&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail-closed sessions&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid CSRF tokens&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The final result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;======================= 87 passed, 2 warnings in 15.65s ========================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1. Can Migrations Build the Database from Nothing?
&lt;/h2&gt;

&lt;p&gt;The first thing I checked was the Alembic migration history.&lt;/p&gt;

&lt;p&gt;Instead of testing only against a database that already existed, I wanted to verify:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Can the current database schema be rebuilt from a completely empty database using only the migration history?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The test creates an isolated SQLite database under &lt;code&gt;tmp_path&lt;/code&gt; and runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Empty database
↓
Alembic base
↓
upgrade head
↓
current schema
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test verifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;products&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;daily_sales&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;alembic_version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;required columns&lt;/li&gt;
&lt;li&gt;the composite unique constraint on &lt;code&gt;daily_sales(product_id, date)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the database revision matches the current Alembic head&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not hard-code the revision ID into the test.&lt;/p&gt;

&lt;p&gt;Instead, the test dynamically checks the current Alembic head.&lt;/p&gt;

&lt;p&gt;This test verifies that the migration chain can rebuild the expected schema in an isolated SQLite database. It does &lt;strong&gt;not&lt;/strong&gt; replace PostgreSQL-specific migration testing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If I compare this to a company, this isn't asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Can the headquarters that already exists keep operating?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"If we start from an empty lot, can we build a new branch correctly using only the blueprints?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Invalid &lt;code&gt;year&lt;/code&gt; / &lt;code&gt;month&lt;/code&gt; Should Return 400, Not 500
&lt;/h2&gt;

&lt;p&gt;Next, I investigated dashboard-related query parameters.&lt;/p&gt;

&lt;p&gt;The affected routes were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dashboard
/api/dashboard-data
/api/ai-advice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the time, values such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?year=abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?month=abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;were passed directly to &lt;code&gt;int()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That meant a &lt;code&gt;ValueError&lt;/code&gt; could occur and potentially result in an HTTP 500 response.&lt;/p&gt;

&lt;p&gt;So I defined the expected behavior as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;non-integer &lt;code&gt;year&lt;/code&gt; or &lt;code&gt;month&lt;/code&gt; values should be rejected with HTTP 400 Bad Request.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 routes × year/month
= 6 cases
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All six were RED at first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected: 400
Actual:   500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then added a small helper that converts query parameters to integers and calls &lt;code&gt;abort(400)&lt;/code&gt; when the value is invalid.&lt;/p&gt;

&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the AI advice API, I also verified that invalid query parameters are rejected &lt;strong&gt;before the Gemini client is called&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Locking Down Gemini Failure Fallbacks
&lt;/h2&gt;

&lt;p&gt;This application uses the Gemini API.&lt;/p&gt;

&lt;p&gt;But Gemini is an external service, so failures can happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;429&lt;/li&gt;
&lt;li&gt;503&lt;/li&gt;
&lt;li&gt;other exceptions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application already had fallback behavior for these cases.&lt;/p&gt;

&lt;p&gt;Stage 4 added regression tests to preserve that behavior.&lt;/p&gt;

&lt;p&gt;I tested:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;429
→ fallback for rate limiting

503
→ fallback for service unavailable

other exception
→ general fallback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did not connect to the real Gemini API.&lt;/p&gt;

&lt;p&gt;All external calls were mocked.&lt;/p&gt;

&lt;p&gt;These tests did not uncover a new bug.&lt;/p&gt;

&lt;p&gt;Instead, the goal was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;to record an existing safety mechanism as regression tests so future changes don't accidentally remove it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;In company terms, this is like asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"If one of our external business partners goes offline, does our own system stay standing?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. Testing the Authenticated AI Advice Happy Path
&lt;/h2&gt;

&lt;p&gt;I also added one test for the normal AI advice flow.&lt;/p&gt;

&lt;p&gt;I wanted to verify that the different parts of the system still worked together correctly when nothing was failing.&lt;/p&gt;

&lt;p&gt;The flow was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication
↓
query processing
↓
SQLite database aggregation
↓
prompt generation
↓
mock Gemini
↓
JSON response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, suppose the database contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;August 2026

Product A: 10
Product B: 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test verifies that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product A and Product B for August are included in the prompt&lt;/li&gt;
&lt;li&gt;products from another month are not included&lt;/li&gt;
&lt;li&gt;the mocked Gemini response is returned as JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Fail Closed: Don't Trust an Old Session After Authentication Settings Change
&lt;/h2&gt;

&lt;p&gt;This was the part of Stage 4 where I spent the most time experimenting.&lt;/p&gt;

&lt;p&gt;The theme was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;fail-closed behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fail-closed means:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;when something is wrong or uncertain, default to the safer state.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A traffic signal is an easy analogy.&lt;/p&gt;

&lt;p&gt;If the signal system breaks, you don't want:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Something is wrong, so let's assume green."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You want:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Something is wrong, so stop."&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The First Problem I Found
&lt;/h3&gt;

&lt;p&gt;I found that an existing authenticated session might still restore the administrator even after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ADMIN_PASSWORD_HASH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;had been changed to an invalid or different value.&lt;/p&gt;

&lt;p&gt;So I created a RED test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Successful login
↓
change ADMIN_PASSWORD_HASH
↓
GET /dashboard
↓
expect 302 redirect to /login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But then another problem appeared.&lt;/p&gt;

&lt;h3&gt;
  
  
  The RED Test Wasn't Following the Path I Thought It Was
&lt;/h3&gt;

&lt;p&gt;In the test environment, the application context remained alive longer than I expected.&lt;/p&gt;

&lt;p&gt;That meant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;g._login_user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;still contained authentication information from the previous request.&lt;/p&gt;

&lt;p&gt;I thought I was testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;session
↓
load_user()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but the request was actually using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;previous request's g._login_user
↓
still authenticated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I learned something important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A test being RED does not automatically mean it is RED for the reason you intended.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I changed the test so that the cached authentication state from the previous request was removed.&lt;/p&gt;

&lt;p&gt;I also used a spy to confirm that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load_user("admin")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;was actually called again.&lt;/p&gt;

&lt;p&gt;Even then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected: 302
Actual:   200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, I had confirmed the real RED condition.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One lesson that stood out during this work was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's not enough to ask whether GREEN is GREEN for the right reason. RED also needs to be RED for the right reason.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  My First Fix Was a Roughly 60-Line Hash Parser
&lt;/h3&gt;

&lt;p&gt;My first implementation tried to validate whether a Werkzeug password hash itself had a valid format.&lt;/p&gt;

&lt;p&gt;I wrote a helper that parsed things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scrypt&lt;/li&gt;
&lt;li&gt;pbkdf2&lt;/li&gt;
&lt;li&gt;digest&lt;/li&gt;
&lt;li&gt;parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tests passed.&lt;/p&gt;

&lt;p&gt;But after reviewing the implementation, another problem became obvious:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;it was far too complicated for what I was actually trying to achieve.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now the application itself would need to understand and maintain Werkzeug's password-hash formats.&lt;/p&gt;

&lt;p&gt;If Werkzeug added or changed supported formats in the future, my own parser might also need to change.&lt;/p&gt;

&lt;p&gt;So I stopped and redesigned the approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Switching to a Fingerprint
&lt;/h3&gt;

&lt;p&gt;The real question I wanted to answer was not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is the current password hash in a valid format?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ADMIN_PASSWORD_HASH at login
==
Current ADMIN_PASSWORD_HASH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So instead, I generate a SHA-256 fingerprint from &lt;code&gt;ADMIN_PASSWORD_HASH&lt;/code&gt; when login succeeds and store that fingerprint in the session.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Successful login
↓
record a "fingerprint" of the current ADMIN_PASSWORD_HASH
↓
next request
↓
generate a fingerprint from the current ADMIN_PASSWORD_HASH
↓
match    → continue
mismatch → require login again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the fingerprint is stored in the session.&lt;/p&gt;

&lt;p&gt;I do &lt;strong&gt;not&lt;/strong&gt; store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the password hash itself&lt;/li&gt;
&lt;li&gt;the real password&lt;/li&gt;
&lt;li&gt;a plaintext password&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fingerprint logic was separated into a small helper.&lt;/p&gt;

&lt;p&gt;In company terms:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;instead of storing the key itself, I added someone who checks the key's fingerprint.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The final two tests verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ADMIN_PASSWORD_HASH changed
→ reject existing session

ADMIN_PASSWORD_HASH unchanged
→ restore session normally
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Can the Application Reject a Tampered CSRF Token?
&lt;/h2&gt;

&lt;p&gt;The final area I checked was CSRF protection.&lt;/p&gt;

&lt;p&gt;The existing pytest suite already verified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No CSRF token
→ reject the POST request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But I had not yet tested:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a token that exists but has been modified or forged.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using an airport analogy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No passport
→ rejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not the only case worth checking.&lt;/p&gt;

&lt;p&gt;I also wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forged passport
→ rejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For these tests, I did not generate an entirely fake token from scratch.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;retrieve a valid CSRF token using the same client&lt;/li&gt;
&lt;li&gt;modify only the first character&lt;/li&gt;
&lt;li&gt;send the POST request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I added three tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Login
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Correct username/password
+
tampered CSRF token
↓
400
↓
authentication not created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Product Registration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tampered CSRF token
↓
400
↓
Product unchanged
DailySales unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sales Registration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tampered CSRF token
↓
400
↓
DailySales unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three tests were GREEN immediately because Flask-WTF was already rejecting the tampered tokens correctly.&lt;/p&gt;

&lt;p&gt;So this wasn't a case where I added a new security feature.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I added evidence to the regression suite that an existing security checkpoint was actually working.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final pytest Result
&lt;/h2&gt;

&lt;p&gt;After all Stage 4 changes, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;collected 87 items

...

======================= 87 passed, 2 warnings in 15.65s ========================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There were no failures or errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 4 Commit List&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bb8b503 test: cover empty database migrations
a1a5578 fix: reject invalid dashboard query parameters
b36ad7a test: cover Gemini failure fallbacks
757ef6f test: cover authenticated AI advice route
412fcb3 fix: invalidate sessions when admin credentials change
26ae4f8 test: cover invalid CSRF tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verifying Everything Again with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;After confirming all 87 tests were GREEN locally, I used the following flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature/pytest-stage4
↓
Pull Request
↓
GitHub Actions
↓
main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Pull Request, I confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;All checks have passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and verified that there were no conflicts before merging.&lt;/p&gt;

&lt;p&gt;After the merge, I updated my local &lt;code&gt;main&lt;/code&gt; branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git pull
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, as an additional check, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge-base &lt;span class="nt"&gt;--is-ancestor&lt;/span&gt; feature/pytest-stage4 main &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK: feature/pytest-stage4 is fully included in main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"NG: main still has missing commits"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OK: feature/pytest-stage4 is fully included in main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SHA values for &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;origin/main&lt;/code&gt; also matched.&lt;/p&gt;

&lt;p&gt;There were no file differences between the feature branch and &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, after the merge, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;again on &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The final result remained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;87 passed, 2 warnings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;In company terms, I think of this as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the safety equipment in the test department&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send it through headquarters review&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approve it for official use&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run all 87 safety checks again at headquarters&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Two Remaining Warnings
&lt;/h2&gt;

&lt;p&gt;There are still two warnings during the migration tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DeprecationWarning:
'get_engine' is deprecated and will be removed in Flask-SQLAlchemy 3.2.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They originate from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;migrations/env.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;get_engine()&lt;/code&gt; is still used.&lt;/p&gt;

&lt;p&gt;The new migration test made these warnings more visible, but they were not introduced by the Stage 4 changes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I am aware of these warnings, but I deliberately left them outside the scope of Stage 4.&lt;/p&gt;

&lt;p&gt;Instead of fixing something just because I happened to notice it, I prefer to treat unrelated changes as separate tasks.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I Learned from Stage 4
&lt;/h2&gt;

&lt;p&gt;The test suite grew from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;71
↓
87
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But once again, the number itself was not the most important part.&lt;/p&gt;

&lt;p&gt;The fail-closed work in particular followed this path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Write a RED test
2. Confirm RED
3. Investigate why it is RED
4. Discover a problem in the test fixture
5. Fix the RED test
6. Confirm the real RED condition
7. Implement a fix
8. Decide the implementation is too complicated
9. Throw away the first implementation
10. Redesign it using a simpler fingerprint approach
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That experience made me think beyond:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Does the code work?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I also needed to ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Do I really want to maintain this implementation in the future?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And pytest has become easier for me to understand when I think of it not merely as a behavior check, but as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a mechanism for checking dangerous places before an incident happens.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That feels very similar to hazard anticipation in logistics.&lt;/p&gt;

&lt;p&gt;Instead of waiting for danger to appear, you constantly ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What might happen next?"&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Next: Stage 5 Will Be a Crash Test
&lt;/h2&gt;

&lt;p&gt;I plan for Stage 5 to be the final stage of this pytest improvement series.&lt;/p&gt;

&lt;p&gt;Up to this point, most of the work has been about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;adding tests and building safety mechanisms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stage 5 reverses the question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Can those safety mechanisms actually detect an accident?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On a feature branch, I plan to temporarily introduce small intentional defects into the code and check whether pytest correctly turns RED.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Intentionally introduce a small defect
↓
Run pytest
↓
Confirm the appropriate test turns RED
↓
Immediately restore the original code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is similar to a manual form of mutation testing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The intentionally broken code will not be committed or pushed.&lt;/p&gt;

&lt;p&gt;I will always restore the original implementation before moving to the next check.&lt;/p&gt;

&lt;p&gt;This is not something I will perform in the production environment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the end of Stage 1, the suite had only 9 tests.&lt;/p&gt;

&lt;p&gt;At the end of Stage 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;87 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the final stage, the question will no longer be:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Do I have 87 tests?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It will be:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Can those 87 tests actually detect the failures they are supposed to catch?"&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/turning-pytest-into-an-incident-prevention-ledger-phase-1-from-3-simple-tests-to-9-2o53"&gt;Stage 1 — From 3 Simple Tests to 9&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-2-strengthening-sales-and-product-57b3"&gt;Stage 2 — Strengthening Sales and Product Registration&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-3-authentication-csrf-and-access-5gi2"&gt;Stage 3 — Authentication, CSRF, and Access Control&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-4-testing-failure-scenarios-71-87-12e7"&gt;Stage 4 — Testing Failure Scenarios&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-5-questioning-green-with-mutation-1e8a"&gt;Stage 5 — Questioning GREEN with Mutation Testing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;a href="https://qiita.com/tosane932" rel="noopener noreferrer"&gt;https://qiita.com/tosane932&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>pytest</category>
      <category>security</category>
    </item>
    <item>
      <title>📝 Growing pytest into an “Incident Prevention Log” — Stage 3: Authentication, CSRF, and Access Control (51 ➡ 69 Tests)</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Fri, 21 Aug 2026 19:02:46 +0000</pubDate>
      <link>https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-3-authentication-csrf-and-access-5gi2</link>
      <guid>https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-3-authentication-csrf-and-access-5gi2</guid>
      <description>&lt;h2&gt;
  
  
  Hello from Japan 🇯🇵
&lt;/h2&gt;

&lt;p&gt;I'm a truck driver in Japan, teaching myself web application development mainly with Python while continuing to work full-time.&lt;/p&gt;

&lt;p&gt;In my personal Flask project, I've been trying to turn pytest into more than just a tool for checking whether the application works.&lt;/p&gt;

&lt;p&gt;I think of it as an &lt;strong&gt;"incident prevention log"&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;a set of regression tests that helps keep previously discovered problems and near misses from silently returning.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Stage 1&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/f3de1e190873a90de39f" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/f3de1e190873a90de39f&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 2&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/b91261e7103df5792f7d" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/b91261e7103df5792f7d&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 3&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/6d1ca5490979c8cf9d62" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/6d1ca5490979c8cf9d62&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 4&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/372270330e73583a227f" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/372270330e73583a227f&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 5&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/85fd24c7baa6fe7c76a7" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/85fd24c7baa6fe7c76a7&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Stage 3 became a fairly long article because it covers authentication, CSRF protection, and access control.&lt;/p&gt;

&lt;p&gt;To make it easier to read, I've placed some of the implementation details inside expandable &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; sections.&lt;/p&gt;

&lt;p&gt;You can follow the overall story without opening them, so feel free to expand only the sections that interest you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So far, I've strengthened the test suite in stages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stage 1: 3 → 9 tests&lt;/li&gt;
&lt;li&gt;Stage 2: 9 → 51 tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Stage 3, I focused mainly on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;authentication, CSRF protection, and access control&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;and increased the regression suite from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;51 passed
↓
69 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But once again, increasing the number of tests was not the goal itself.&lt;/p&gt;

&lt;p&gt;What I wanted to check in Stage 3 was whether situations like these were possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can anyone modify application data?

Even if someone is logged in,
can an external site cause an unintended POST request?

Can anonymous users access application pages or APIs
that should require authentication?

Can an anonymous user trigger the AI API?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As in the previous stages, I worked with Codex in this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Investigate
↓
RED
↓
Smallest necessary fix
↓
GREEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Stage 3 Started with 51 Passing Tests
&lt;/h2&gt;

&lt;p&gt;At the end of Stage 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;51 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The existing pytest suite already covered areas such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sales input validation&lt;/li&gt;
&lt;li&gt;product registration validation&lt;/li&gt;
&lt;li&gt;invalid product IDs&lt;/li&gt;
&lt;li&gt;products belonging to another month&lt;/li&gt;
&lt;li&gt;discontinued products&lt;/li&gt;
&lt;li&gt;database unique constraints&lt;/li&gt;
&lt;li&gt;rollback when &lt;code&gt;commit()&lt;/code&gt; fails&lt;/li&gt;
&lt;li&gt;soft deletion and preservation of sales history&lt;/li&gt;
&lt;li&gt;dashboard aggregation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, when I investigated authentication-related behavior, I found that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication
CSRF protection
Access control
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;were still not sufficiently implemented.&lt;/p&gt;

&lt;p&gt;So I decided to work through Stage 3 in this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication
↓
CSRF
↓
Access control for application pages and APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  First, I Investigated the Current State Without Fixing Anything
&lt;/h2&gt;

&lt;p&gt;The first thing I asked Codex to do was &lt;strong&gt;not&lt;/strong&gt; to implement authentication.&lt;/p&gt;

&lt;p&gt;I asked it only to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;investigate the current authentication state.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At that point, the application had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no login page&lt;/li&gt;
&lt;li&gt;no User model&lt;/li&gt;
&lt;li&gt;no Flask-Login&lt;/li&gt;
&lt;li&gt;no CSRF tokens&lt;/li&gt;
&lt;li&gt;anonymous access to application pages and APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For state-changing POST requests in particular:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /
POST /input
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;could be reached without authentication.&lt;/p&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;an unauthenticated user could reach the code that modifies product and sales data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of fixing that immediately, I first decided to express the dangerous state as failing pytest tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Authentication: Make Unauthenticated POST Requests RED
&lt;/h2&gt;

&lt;p&gt;The first tests I added were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reject unauthenticated product POST requests.

Reject unauthenticated sales POST requests.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The future behavior I wanted was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;anonymous
↓
POST
↓
302
↓
/login
↓
no database changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before implementing authentication, however:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected: 302
Actual:   200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the request actually reached the database modification logic.&lt;/p&gt;

&lt;p&gt;That was exactly the RED state I wanted to confirm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧪 Authentication: From RED Tests to Implementation&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  I Defined the Login Behavior with RED Tests First
&lt;/h3&gt;

&lt;p&gt;Next, I added tests for the behavior I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /login&lt;/code&gt; should render successfully&lt;/li&gt;
&lt;li&gt;correct credentials should log the user in&lt;/li&gt;
&lt;li&gt;an incorrect password should not log the user in&lt;/li&gt;
&lt;li&gt;the login state should persist in the same test client&lt;/li&gt;
&lt;li&gt;unauthenticated POST requests should be rejected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because no login functionality existed yet:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Only after confirming that RED state did I move on to implementing authentication.&lt;/p&gt;
&lt;h3&gt;
  
  
  I Chose a Single-Admin Login
&lt;/h3&gt;

&lt;p&gt;For this application, I deliberately did not expand the scope into full multi-user management.&lt;/p&gt;

&lt;p&gt;Instead, I chose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;single-admin authentication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;using Flask-Login.&lt;/p&gt;

&lt;p&gt;The credentials are loaded from environment variables:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SECRET_KEY
ADMIN_USERNAME
ADMIN_PASSWORD_HASH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Rather than storing a production plaintext password in the source code, I verify a password hash using:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;check_password_hash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;At this stage, I did &lt;strong&gt;not&lt;/strong&gt; add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a User database model&lt;/li&gt;
&lt;li&gt;general users&lt;/li&gt;
&lt;li&gt;roles&lt;/li&gt;
&lt;li&gt;tenants&lt;/li&gt;
&lt;li&gt;store-specific permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the current specification:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authenticated user
=
single administrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;So Stage 3 does not implement detailed role-based authorization.&lt;/p&gt;

&lt;p&gt;The target here was simply:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;only the authenticated administrator can access protected application functionality.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  After Authentication: 56 Tests GREEN
&lt;/h3&gt;

&lt;p&gt;After implementing the single-admin authentication:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest test_auth.py -v
→ 5 passed

pytest -v
→ 56 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;At that point, I created a local commit:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feat: add single-admin authentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;I wanted to preserve the point where authentication alone was GREEN instead of continuing straight into CSRF changes in the same step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next, I Investigated CSRF Protection
&lt;/h2&gt;

&lt;p&gt;Adding authentication does not automatically mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The application is now safe."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When authentication uses a session cookie, the browser automatically includes that cookie with matching requests.&lt;/p&gt;

&lt;p&gt;That means I also needed to consider whether:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a logged-in user's session could be abused to send a POST request that the user did not intend.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the next step was CSRF protection.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Created RED Tests for CSRF Before Implementing It
&lt;/h2&gt;

&lt;p&gt;I added six CSRF-related tests.&lt;/p&gt;

&lt;p&gt;Before implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6 failed, 56 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The forms had no CSRF token.&lt;/p&gt;

&lt;p&gt;And while authenticated, product and sales POST requests still worked without a token.&lt;/p&gt;

&lt;p&gt;Even the login POST succeeded with the correct username and password when no CSRF token was supplied.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧪 The Six CSRF RED Tests and Implementation Details&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  CSRF Tests I Added
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The login form contains a CSRF token.

The product form contains a CSRF token.

The sales form contains a CSRF token.

Reject a login POST without a token.

Reject a product POST without a token.

Reject a sales POST without a token.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Without a Token, the Request Could Still Modify the Database
&lt;/h3&gt;

&lt;p&gt;Before adding CSRF protection, a product POST without a token resulted in:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status=200
products_unchanged=False
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;In other words:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a request without a CSRF token could still reach the Product modification logic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The sales POST behaved similarly:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status=200
sales_unchanged=False
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;and &lt;code&gt;DailySales&lt;/code&gt; was modified.&lt;/p&gt;

&lt;p&gt;Instead of stopping at the observation:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;There is no CSRF protection.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;I wanted the RED tests to show:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;how far the current request could actually proceed.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Adding &lt;code&gt;CSRFProtect&lt;/code&gt; with Flask-WTF
&lt;/h3&gt;

&lt;p&gt;I used Flask-WTF for CSRF protection.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;CSRFProtect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Then I added a hidden CSRF field to the three POST forms:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The protected forms were:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/login
/
/input
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;I did not add CSRF exemptions.&lt;/p&gt;

&lt;p&gt;I also did not disable CSRF protection in the test environment.&lt;/p&gt;
&lt;h3&gt;
  
  
  Existing Tests Also Had to Become "Legitimate POST Requests"
&lt;/h3&gt;

&lt;p&gt;Once &lt;code&gt;CSRFProtect&lt;/code&gt; was enabled, existing positive-path tests also returned 400 if they posted without a token.&lt;/p&gt;

&lt;p&gt;But tests intended to verify things such as:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product validation

sales rollback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;need to reach the business logic.&lt;/p&gt;

&lt;p&gt;If CSRF blocks them first, those tests are no longer testing what they are supposed to test.&lt;/p&gt;

&lt;p&gt;So I changed the test flow to:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET
↓
retrieve csrf_token
↓
POST with a valid token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;I created a test fixture for retrieving CSRF tokens.&lt;/p&gt;

&lt;p&gt;The authenticated test client also does not directly inject a logged-in state into the session.&lt;/p&gt;

&lt;p&gt;Instead, it follows a flow closer to the real login process:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /login
↓
retrieve CSRF token
↓
username
password
csrf_token
↓
POST /login
↓
authenticated client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  After CSRF Protection: 62 Tests GREEN
&lt;/h3&gt;

&lt;p&gt;After implementation:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest test_csrf.py -v
→ 6 passed

pytest test_auth.py -v
→ 5 passed

pytest -v
→ 62 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;For product and sales POST requests without a token:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CSRFProtect rejects the request with 400
↓
no database changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;For the product POST, I compare snapshots of both &lt;code&gt;Product&lt;/code&gt; and &lt;code&gt;DailySales&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the sales POST, I verify that &lt;code&gt;DailySales&lt;/code&gt; is unchanged.&lt;/p&gt;

&lt;p&gt;For login without a token:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;400
↓
no authenticated session is created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;I then created another commit:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feat: add CSRF protection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Even After Adding Authentication and CSRF, Anonymous Users Could Still Access Application Pages
&lt;/h2&gt;

&lt;p&gt;After adding authentication and CSRF protection, I investigated the GET routes and APIs.&lt;/p&gt;

&lt;p&gt;The routes I checked were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/login
/
/input
/dashboard
/api/dashboard-data
/api/ai-advice
/api/greeting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, anonymous requests to all of them returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/login&lt;/code&gt; should of course remain public.&lt;/p&gt;

&lt;p&gt;But the application pages and APIs that were intended to require authentication were still accessible anonymously.&lt;/p&gt;




&lt;h2&gt;
  
  
  Anonymous Users Could Even Reach the AI API
&lt;/h2&gt;

&lt;p&gt;The endpoints that concerned me most were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/ai-advice
/api/greeting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the investigation, I did not connect to the real Gemini API.&lt;/p&gt;

&lt;p&gt;Instead, I mocked the Gemini client.&lt;/p&gt;

&lt;p&gt;The result was that an anonymous request still caused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gemini Client
→ called once
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So under the right conditions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;an unauthenticated user could reach the AI-processing logic.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Access Control Also Started with RED Tests
&lt;/h2&gt;

&lt;p&gt;So I added a new test file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_authorization.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filename uses &lt;code&gt;authorization&lt;/code&gt;, but I want to be precise about what it means in this project.&lt;/p&gt;

&lt;p&gt;I was &lt;strong&gt;not&lt;/strong&gt; testing detailed role-based authorization.&lt;/p&gt;

&lt;p&gt;What I was testing was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;access control that prevents anonymous users from reaching application pages and APIs that should require authentication.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The expected behavior became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /login
→ anonymous 200

All other application pages/APIs that require authentication
→ anonymous 302
→ /login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initial result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 passed, 6 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the public &lt;code&gt;/login&lt;/code&gt; route was GREEN.&lt;/p&gt;

&lt;p&gt;The other six routes still returned HTTP 200 to anonymous users, so they were RED.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔐 Access Control Implementation and Existing Test Adjustments&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Codex Stopped When It Realized Existing Tests Needed Changes
&lt;/h3&gt;

&lt;p&gt;Something memorable happened at this point.&lt;/p&gt;

&lt;p&gt;While preparing to implement access control, Codex determined that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;some existing tests would also need to change.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And it stopped before implementing anything.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_dashboard.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;was checking API aggregation using an anonymous client.&lt;/p&gt;

&lt;p&gt;Once the API became authentication-protected, that client would receive a 302 redirect and never reach the aggregation logic.&lt;/p&gt;

&lt;p&gt;Some CSRF tests similarly retrieved tokens from application pages while anonymous.&lt;/p&gt;

&lt;p&gt;So I reorganized the responsibilities:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_authorization.py
→ verifies anonymous users cannot enter protected areas

test_dashboard.py
→ verifies correct aggregation after authentication

test_csrf.py
→ verifies CSRF behavior

test_auth.py
→ verifies authentication behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;I had already been instructing Codex:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;if an unexpected change becomes necessary, do not continue automatically — report it first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This time, it followed that rule.&lt;/p&gt;

&lt;p&gt;It stopped instead of silently modifying existing tests, so I reviewed the reason and then allowed only the minimum necessary changes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Protecting Six Routes with &lt;code&gt;login_required&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;In the end, I added &lt;code&gt;login_required&lt;/code&gt; to:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/
/input
/dashboard
/api/dashboard-data
/api/ai-advice
/api/greeting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;After implementation:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /login
→ anonymous 200

Other six routes
→ anonymous 302
→ /login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;For the AI endpoints, I also added regression tests confirming that, during anonymous access:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gemini Client
→ 0 calls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;So an anonymous user no longer reaches the AI-processing code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 3 Finished with 69 GREEN Tests
&lt;/h2&gt;

&lt;p&gt;The progression was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;51 passed
↓
56 passed
↓
62 passed
↓
69 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stage 3 directly added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication     5 tests
CSRF               6 tests
Access control     7 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for a total of 18 additional tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;51
↓
69
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;✅ Final pytest Results and Test Responsibilities&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Final pytest Results
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest test_authorization.py -v
→ 7 passed

pytest test_auth.py -v
→ 5 passed

pytest test_csrf.py -v
→ 6 passed

pytest test_dashboard.py -v
→ 4 passed

pytest -v
→ 69 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  The Tests Started to Develop Clear Responsibilities
&lt;/h3&gt;

&lt;p&gt;One of the biggest things I noticed during Stage 3 was not simply that the test count increased.&lt;/p&gt;

&lt;p&gt;It was that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;each group of tests started to have a clearer responsibility.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_auth.py
→ login behavior and rejection of unauthenticated POST requests

test_csrf.py
→ CSRF tokens and rejection of tokenless POST requests

test_authorization.py
→ anonymous users cannot access protected pages/APIs

test_dashboard.py
→ aggregation after authentication

test_products.py
→ product behavior after authentication and CSRF checks

test_sales.py
→ sales behavior after authentication and CSRF checks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Earlier, many of my pytest tests were closer to:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A response came back.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Now they were gradually becoming:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Which specific incident is this test responsible for stopping?"&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  I Also Used a Feature Branch and Pull Request
&lt;/h2&gt;

&lt;p&gt;For Stage 3, I worked on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature/auth-hardening
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After confirming all 69 tests were GREEN locally, I followed this flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature/auth-hardening
↓
push to GitHub
↓
Pull Request
↓
GitHub Actions
↓
merge into main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had already been using local pytest and GitHub Actions.&lt;/p&gt;

&lt;p&gt;But this time, I also went through the process of:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;creating a Pull Request from a feature branch, confirming CI success, and only then merging into &lt;code&gt;main&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚚 Commit, Pull Request, and GitHub Actions Details&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  I Split the Commits by Stage
&lt;/h3&gt;

&lt;p&gt;I used separate commits:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feat: add single-admin authentication

feat: add CSRF protection

feat: protect authenticated routes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;In the Pull Request, I checked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;base: &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;compare: &lt;code&gt;feature/auth-hardening&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;three commits&lt;/li&gt;
&lt;li&gt;the diff&lt;/li&gt;
&lt;li&gt;whether there were merge conflicts&lt;/li&gt;
&lt;li&gt;GitHub Actions results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after that did I merge.&lt;/p&gt;
&lt;h3&gt;
  
  
  GitHub Actions Also Passed All 69 Tests
&lt;/h3&gt;

&lt;p&gt;After creating the Pull Request, GitHub Actions ran &lt;code&gt;Run Tests&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;CI collected:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;collected 69 items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;and finished with:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;69 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;GitHub Actions reported:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Only after confirming that result did I merge into &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the Stage 3 flow became:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Local pytest
69 passed
↓
Push feature branch
↓
Pull Request
↓
GitHub Actions
69 passed
↓
Merge into main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  I Ran All 69 Tests Again After the Merge
&lt;/h3&gt;

&lt;p&gt;After merging the Pull Request, I switched my local environment back to &lt;code&gt;main&lt;/code&gt; and synchronized it with &lt;code&gt;origin/main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I confirmed that the local and remote &lt;code&gt;main&lt;/code&gt; HEADs matched.&lt;/p&gt;

&lt;p&gt;Then I ran:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest -v
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;one more time.&lt;/p&gt;

&lt;p&gt;The result:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;69 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The working tree was also clean.&lt;/p&gt;

&lt;p&gt;At that point, I considered Stage 3 complete.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Pull Request Feels Like a Shipping Checkpoint to Me
&lt;/h2&gt;

&lt;p&gt;I've previously compared pytest to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a pre-departure inspection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;and GitHub Actions to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;an automated inspection at the shipping gate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using Pull Requests added another checkpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;working branch
↓
Pull Request
↓
CI
↓
main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thinking about it through my work as a truck driver, it feels something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inspect in the work area
↓
Check whether it is ready to ship
↓
Automated inspection
↓
Send it onto the main route
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It works locally, so put it straight into main."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I now have another place to review the diff and test results before the code enters &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Using the process myself helped me understand why that extra checkpoint matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  State of the Application at the End of Stage 3
&lt;/h2&gt;

&lt;p&gt;At the end of Stage 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Single-admin authentication
✅

CSRF protection
✅

Anonymous access blocked for protected pages/APIs
✅

Anonymous AI API execution blocked
✅

pytest
69 passed

GitHub Actions
69 passed / Success

Pull Request
✅

Merged into main
✅

Post-merge main
69 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the scope I defined:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 3 was complete.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Deliberately Did Not Do in Stage 3
&lt;/h2&gt;

&lt;p&gt;During the final review, I also found several things where I decided:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I found this, but I'm not fixing it in this stage."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If I expanded into every improvement I discovered, the goal of Stage 3 would become unclear.&lt;/p&gt;

&lt;p&gt;So I kept the scope to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Single-admin authentication
CSRF protection
Blocking anonymous access to protected pages/APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;📋 Improvements I Deliberately Left for Later&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The final review identified possible future improvements such as:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logout
dedicated fail-closed tests for missing configuration
dedicated invalid-CSRF-token tests
Session Cookie settings
login attempt rate limiting
JSON 401 responses for APIs
cleaning up duplicated authentication checks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Logout Is Not Implemented Yet
&lt;/h3&gt;

&lt;p&gt;At this point, there is still no:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logout route
logout_user()
logout button
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Anonymous access protection itself works.&lt;/p&gt;

&lt;p&gt;However, because the authentication system uses a Session Cookie, allowing an administrator to explicitly end a session is something I want to add later.&lt;/p&gt;

&lt;p&gt;This becomes especially important if the application is used on a shared device.&lt;/p&gt;

&lt;p&gt;I plan to reconsider it before a real production rollout.&lt;/p&gt;
&lt;h3&gt;
  
  
  There Is No Detailed Role-Based Authorization Yet
&lt;/h3&gt;

&lt;p&gt;The current application still has no:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User database model&lt;/li&gt;
&lt;li&gt;general users&lt;/li&gt;
&lt;li&gt;roles&lt;/li&gt;
&lt;li&gt;tenants&lt;/li&gt;
&lt;li&gt;store IDs&lt;/li&gt;
&lt;li&gt;store-level access control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only user who can log in is the single administrator configured through environment variables.&lt;/p&gt;

&lt;p&gt;So Stage 3 assumes:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authenticated user
=
administrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;and considers the following sufficient for this stage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;only the authenticated administrator can reach protected application functionality.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If I introduce multiple users in the future, I will need to design role-based and store-level permissions separately.&lt;/p&gt;
&lt;h3&gt;
  
  
  Production Requires Environment Variables
&lt;/h3&gt;

&lt;p&gt;The authentication implementation reads:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SECRET_KEY
ADMIN_USERNAME
ADMIN_PASSWORD_HASH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;from environment variables.&lt;/p&gt;

&lt;p&gt;So these values must be configured correctly before using authentication in production.&lt;/p&gt;

&lt;p&gt;In particular:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ADMIN_PASSWORD_HASH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;expects a Werkzeug-compatible password hash, not a plaintext password.&lt;/p&gt;

&lt;p&gt;Stage 3 did not include changing the production environment configuration on Render.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Stage 3 of strengthening pytest moved the suite from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;51 passed
↓
69 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main additions were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;single-admin authentication&lt;/li&gt;
&lt;li&gt;CSRF protection&lt;/li&gt;
&lt;li&gt;access control for protected pages and APIs&lt;/li&gt;
&lt;li&gt;preventing anonymous AI API execution&lt;/li&gt;
&lt;li&gt;regression tests for authentication, CSRF, and access control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stage 1 began with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;turning only three pytest tests into the beginning of a regression suite.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stage 2 expanded into:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;database updates, rollback behavior, soft deletion, and aggregation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And Stage 3 expanded the question again.&lt;/p&gt;

&lt;p&gt;It was no longer only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can the application save the correct data?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also became:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Who is allowed to reach the code that performs that operation?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was also the first stage where I actually used the full flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature branch
↓
Pull Request
↓
GitHub Actions
↓
merge into main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are still areas I can improve.&lt;/p&gt;

&lt;p&gt;But by continuing with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Investigate
↓
RED
↓
Smallest necessary fix
↓
GREEN
↓
Commit
↓
Next problem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pytest has gradually started to feel less like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;something I run after an incident&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;and more like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a mechanism that catches known failure conditions before they can cause the same problem again.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next, I plan to move on to Stage 4.&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/turning-pytest-into-an-incident-prevention-ledger-phase-1-from-3-simple-tests-to-9-2o53"&gt;Stage 1 — From 3 Simple Tests to 9&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-2-strengthening-sales-and-product-57b3"&gt;Stage 2 — Strengthening Sales and Product Registration&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-3-authentication-csrf-and-access-5gi2"&gt;Stage 3 — Authentication, CSRF, and Access Control&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-4-testing-failure-scenarios-71-87-12e7"&gt;Stage 4 — Testing Failure Scenarios&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-5-questioning-green-with-mutation-1e8a"&gt;Stage 5 — Questioning GREEN with Mutation Testing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932" rel="noopener noreferrer"&gt;https://qiita.com/tosane932&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>pytest</category>
      <category>security</category>
      <category>codex</category>
    </item>
    <item>
      <title>🐕‍🦺 Don’t Let Codex “Eat Things Off the Ground”: Preventing Scope Creep in pytest Stage 3</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Fri, 21 Aug 2026 10:17:55 +0000</pubDate>
      <link>https://dev.to/tosane932/dont-let-codex-eat-things-off-the-ground-preventing-scope-creep-in-pytest-stage-3-15d7</link>
      <guid>https://dev.to/tosane932/dont-let-codex-eat-things-off-the-ground-preventing-scope-creep-in-pytest-stage-3-15d7</guid>
      <description>&lt;h2&gt;
  
  
  Hello from Japan 🇯🇵
&lt;/h2&gt;

&lt;p&gt;I'm a truck driver in Japan, teaching myself web application development with Python and Flask while continuing to work full-time.&lt;/p&gt;

&lt;p&gt;This article is part of my ongoing record of strengthening pytest in my personal Flask project.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Recently, I've been using Codex in VS Code while strengthening pytest in a Flask application I'm building.&lt;/p&gt;

&lt;p&gt;I'm currently working on Stage 3.&lt;/p&gt;

&lt;p&gt;This stage focuses on authentication, authorization, CSRF protection, and questions such as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Who is allowed to perform this operation?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Was this action actually intended by the user?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While working with Codex on this, a strange analogy suddenly came to mind:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Isn't this a bit like a dog eating random things off the ground during a walk? 🦮&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Japanese, there's a word for this: &lt;strong&gt;拾い食い (hiroi-gui)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It describes a dog finding something on the ground and eating it without waiting for its owner.&lt;/p&gt;

&lt;p&gt;And the more I worked with Codex, the more similar it started to feel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Codex Finds Problems I Didn't Ask It to Find
&lt;/h2&gt;

&lt;p&gt;When I ask Codex to investigate code, it sometimes finds problems outside the scope of the current task.&lt;/p&gt;

&lt;p&gt;For example, even if I say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Only investigate authentication this time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex may respond with things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;There is also a problem in another API.

This process could also be improved.

This part probably needs to be fixed as well.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That isn't a bad thing.&lt;/p&gt;

&lt;p&gt;In fact:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;having it discover problems that I didn't notice myself is extremely useful.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But the situation changes when it goes one step further and says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I fixed that while I was at it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's where I become cautious.&lt;/p&gt;




&lt;h2&gt;
  
  
  "While I'm Here, I'll Fix This Too" Started Looking Like a Dog Eating Things Off the Ground
&lt;/h2&gt;

&lt;p&gt;This is the analogy that came to mind while working with Codex.&lt;/p&gt;

&lt;p&gt;Imagine a dog out for a walk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Finds something on the ground
↓
Gets curious
↓
Puts it in its mouth without asking the owner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the behavior I'm talking about.&lt;/p&gt;

&lt;p&gt;Codex can sometimes behave similarly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Investigates the code
↓
Finds another problem
↓
Decides it looks worth fixing
↓
Fixes it while it's there
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From its perspective, it may feel like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I did something helpful! 🐶"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But from the owner's perspective:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Wait, wait!&lt;br&gt;&lt;br&gt;
Where did you get that?&lt;br&gt;&lt;br&gt;
You can't just eat random things!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And with Codex, my reaction becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Wait, wait!&lt;br&gt;&lt;br&gt;
I didn't ask you to touch that this time!&lt;br&gt;&lt;br&gt;
Don't expand the scope on your own!&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Finding Problems Is Fine ⭕ — Fixing Them Without Asking Is Not ✖
&lt;/h2&gt;

&lt;p&gt;Recently, I've started making my instructions to Codex much more explicit.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If you discover an unexpected problem,
do not expand the scope of the current task.

Report the problem only and stop.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Finding a problem is fine.&lt;br&gt;&lt;br&gt;
Fixing it without permission is not.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When a new issue is discovered, I want the flow to look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Problem discovered
↓
Report it
↓
Stop the current work
↓
Human reviews it
↓
If necessary, create a separate task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets me separate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;discovering a problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;deciding to fix it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Are Unplanned Fixes Risky?
&lt;/h2&gt;

&lt;p&gt;At first glance, you might think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you've already found the problem, wouldn't it be faster to fix it at the same time?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I thought the same thing at first.&lt;/p&gt;

&lt;p&gt;But after working through several changes, I started seeing some problems with this approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Scope of the Change Becomes Hard to Understand
&lt;/h3&gt;

&lt;p&gt;Imagine that the original task was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add authentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the final change includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication
API fixes
Database changes
Template changes
Logging improvements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it becomes much harder to understand:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;which change was made for which purpose.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  When Something Breaks, Finding the Cause Becomes Harder
&lt;/h3&gt;

&lt;p&gt;If there is only one change, I can reasonably think:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This change is probably what caused the problem.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if several unrelated fixes are mixed together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which change caused it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes much harder to answer.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Meaning of the Tests Also Becomes Less Clear
&lt;/h3&gt;

&lt;p&gt;Recently, I've been thinking of pytest as an:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"incident prevention log."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I discover one problem, I try to follow this process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reproduce the dangerous state with a test
↓
Confirm that the test fails
↓
Make the smallest necessary fix
↓
Confirm that the test passes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if I also fix unrelated problems in the middle of that process, it becomes harder to tell:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;which code change was actually intended to make which failing test pass.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  I Used "Wait 🖐" During pytest Stage 3 Too
&lt;/h2&gt;

&lt;p&gt;In the current Stage 3 of my pytest work, I'm focusing on authentication-related behavior.&lt;/p&gt;

&lt;p&gt;First, without changing any production code, I investigated the current state of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication
Authorization
CSRF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I added a future requirement as a failing test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reject unauthenticated POST requests.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also added login behavior as another failing test.&lt;/p&gt;

&lt;p&gt;At that point, I confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after that did I move on to the smallest authentication implementation needed.&lt;/p&gt;

&lt;p&gt;After the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;56 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During this process, I kept the scope deliberately narrow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication only this time

Do not touch CSRF yet

Do not touch logout yet

Do not protect the APIs yet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when Codex could clearly continue into the next area:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I stopped it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  "It Can Do It" and "It Should Do It Now" Are Different
&lt;/h2&gt;

&lt;p&gt;One thing became especially clear to me during this stage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;what Codex can do and what Codex should do right now are two different things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;It can implement CSRF protection.

It can add logout.

It can protect the APIs.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that does not mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do all of them now.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The human still needs to decide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This is where today's task ends.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I think this applies far beyond programming.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;git status&lt;/code&gt; and &lt;code&gt;git diff&lt;/code&gt; Are Like Checking the Dog's Mouth After a Walk
&lt;/h2&gt;

&lt;p&gt;Imagine your dog comes home from a walk.&lt;/p&gt;

&lt;p&gt;You might wonder:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did you eat anything strange out there?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After Codex finishes working, I do something similar by checking the Git state and diff.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status &lt;span class="nt"&gt;--short&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--stat&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use these to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether only the requested files were modified&lt;/li&gt;
&lt;li&gt;whether unexpected files appeared&lt;/li&gt;
&lt;li&gt;whether unrelated fixes were mixed in&lt;/li&gt;
&lt;li&gt;whether there are whitespace errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my head, it looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Codex:
"I finished the walk properly! 🐶"

git diff:
"Okay... let me see what's in your mouth."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Even if the AI reports that there were no changes outside the requested scope, I still check the diff myself.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  pytest Is Not a Tool for Checking Whether the Dog Ate Something
&lt;/h2&gt;

&lt;p&gt;I run pytest too.&lt;/p&gt;

&lt;p&gt;But pytest is &lt;strong&gt;not&lt;/strong&gt; primarily a tool for checking whether Codex made unauthorized changes.&lt;/p&gt;

&lt;p&gt;What pytest checks is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did we break behavior that the application was already supposed to preserve?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So these checks have different jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Task scope
↓
git diff
↓
pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my head:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Task scope
= deciding the walking route

Stop on unexpected issues
= don't eat random things

git diff
= checking after the walk

pytest
= checking existing rules and system health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one protects a different part of the workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Don't Let Codex Roam Free
&lt;/h2&gt;

&lt;p&gt;I previously wrote another article about not letting Codex &lt;strong&gt;"roam free."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://dev.to/tosane932/dont-let-codex-roam-free-6-guardrails-i-use-for-ai-assisted-coding-4bj4"&gt;Don't Let Codex Roam Free: 6 Guardrails I Use for AI-Assisted Coding&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This idea of preventing it from "eating things off the ground" is really an extension of the same approach.&lt;/p&gt;

&lt;p&gt;Codex can work very quickly.&lt;/p&gt;

&lt;p&gt;That also means it can move through a sequence like this very quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Investigate
↓
Discover a problem
↓
Fix it
↓
Discover another problem
↓
Fix that too
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's exactly why I think:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the faster an AI can move, the more important it is to define where it must stop.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  An Instruction I Use More Often Now
&lt;/h2&gt;

&lt;p&gt;Recently, I've started giving Codex instructions like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If you discover an unexpected problem,
do not expand the scope of the current task.

Report it and stop.

Do not continue to the next task.

Do not commit or push.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That may sound overly cautious.&lt;/p&gt;

&lt;p&gt;But it isn't really because I distrust Codex.&lt;/p&gt;

&lt;p&gt;It's more that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;because Codex can move so quickly, I want its working boundaries to be explicit.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;While working through pytest Stage 3 with Codex, I realized:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex making unplanned "while I'm here" fixes feels a little like a dog eating random things off the ground. 🐩&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My current rules for preventing that are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define the scope of the current task&lt;/li&gt;
&lt;li&gt;report unexpected problems instead of fixing them automatically&lt;/li&gt;
&lt;li&gt;separate discovery from fixing&lt;/li&gt;
&lt;li&gt;don't let the AI move into the next task on its own&lt;/li&gt;
&lt;li&gt;inspect the actual changes with &lt;code&gt;git diff&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;use pytest to verify that existing behavior still works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important rule is not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Don't find problems."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Actually, I want Codex to find them.&lt;/p&gt;

&lt;p&gt;What I really mean is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find them.&lt;br&gt;&lt;br&gt;
Just don't eat them without asking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Codex is a very capable dog. 🐾&lt;/p&gt;

&lt;p&gt;And for now, I've found that I work best with it when I'm able to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Wait.&lt;br&gt;&lt;br&gt;
We're not going there yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, I plan to continue Stage 3 by working on CSRF protection.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932" rel="noopener noreferrer"&gt;https://qiita.com/tosane932&lt;/a&gt;&lt;/p&gt;

</description>
      <category>codex</category>
      <category>ai</category>
      <category>pytest</category>
      <category>git</category>
    </item>
    <item>
      <title>📝Growing pytest into an "Incident Prevention Log" — Stage 2: Strengthening Sales and Product Registration to 51 Tests</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:13:49 +0000</pubDate>
      <link>https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-2-strengthening-sales-and-product-57b3</link>
      <guid>https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-2-strengthening-sales-and-product-57b3</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hello from Japan 🇯🇵
&lt;/h2&gt;

&lt;p&gt;I’m a truck driver in Japan, and I’m teaching myself web application development with Python and Flask while continuing to work full-time.&lt;/p&gt;

&lt;p&gt;This article is part of a series documenting how I’ve been strengthening pytest in my personal project — not just to increase the number of tests, but to turn them into a kind of “incident prevention log.”&lt;/p&gt;

&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Stage 1&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/f3de1e190873a90de39f" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/f3de1e190873a90de39f&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 2&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/b91261e7103df5792f7d" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/b91261e7103df5792f7d&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 3&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/6d1ca5490979c8cf9d62" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/6d1ca5490979c8cf9d62&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 4&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/372270330e73583a227f" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/372270330e73583a227f&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stage 5&lt;br&gt;&lt;br&gt;
&lt;a href="https://qiita.com/tosane932/items/85fd24c7baa6fe7c76a7" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/85fd24c7baa6fe7c76a7&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I currently work as a truck driver while teaching myself web application development with Python and Flask.&lt;/p&gt;

&lt;p&gt;At the time I wrote the original version of this article, I had logged 167 hours of study.&lt;/p&gt;

&lt;p&gt;When I reviewed the pytest suite in my personal project, I realized that there were only three tests.&lt;/p&gt;

&lt;p&gt;That made me wonder:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I have tests, but are they actually protecting the important parts of the application?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I decided to stop thinking of pytest as just a tool for checking whether something works.&lt;/p&gt;

&lt;p&gt;Instead, I started treating it as an:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"incident prevention log" — a record that helps prevent previously discovered problems from silently returning.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Stage 1, I increased the number of tests from 3 to 9 and added checks around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prompts sent to the AI&lt;/li&gt;
&lt;li&gt;XSS protection&lt;/li&gt;
&lt;li&gt;Gemini API integration using mocks&lt;/li&gt;
&lt;li&gt;running the full pytest suite with GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article continues from there with Stage 2.&lt;/p&gt;

&lt;p&gt;By the end of this stage, the full suite had grown to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;51 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But increasing the number of tests was not the main goal.&lt;/p&gt;

&lt;p&gt;What I really wanted to verify was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;when handling sales and product data, invalid input or database save failures should not leave the database in an inconsistent state.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Checked in Stage 2
&lt;/h2&gt;

&lt;p&gt;Broadly, I focused on the following areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rejecting invalid sales input&lt;/li&gt;
&lt;li&gt;preventing sales from being registered against the wrong product&lt;/li&gt;
&lt;li&gt;preventing duplicate sales for the same product and date at the database level&lt;/li&gt;
&lt;li&gt;rolling back changes when database saves fail&lt;/li&gt;
&lt;li&gt;rejecting invalid product registration and update input&lt;/li&gt;
&lt;li&gt;preserving historical sales after a product is discontinued&lt;/li&gt;
&lt;li&gt;verifying that dashboard aggregation matches the database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'll just add a few more tests around sales input."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But as I checked each part individually, I realized that the flow connected all the way through:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;input → database save → history preservation → aggregated output&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the scope became much larger than I originally expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. If Even One Sales Entry Is Invalid, Reject the Entire Request
&lt;/h2&gt;

&lt;p&gt;The first thing I checked was the data submitted from the sales input form.&lt;/p&gt;

&lt;p&gt;In this article, I'll refer to the Flask endpoint that receives the submitted form data as the &lt;strong&gt;sales POST endpoint&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, imagine submitting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product A: 5 units
Product B: 3 units
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flask receives that data and saves it to the database.&lt;/p&gt;

&lt;p&gt;The problem was what happened when:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;only part of the submitted data was invalid.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The cases I checked included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalid dates&lt;/li&gt;
&lt;li&gt;empty quantities&lt;/li&gt;
&lt;li&gt;non-numeric quantities&lt;/li&gt;
&lt;li&gt;negative quantities&lt;/li&gt;
&lt;li&gt;decimal quantities&lt;/li&gt;
&lt;li&gt;mismatched numbers of product IDs and quantities&lt;/li&gt;
&lt;li&gt;empty product IDs&lt;/li&gt;
&lt;li&gt;non-numeric product IDs&lt;/li&gt;
&lt;li&gt;duplicate product IDs&lt;/li&gt;
&lt;li&gt;empty product and quantity arrays&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before the fix, there were paths where decimal values could be converted to integers, or where invalid rows were skipped while valid rows were still saved.&lt;/p&gt;

&lt;p&gt;So I changed the policy to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If even one value is invalid, reject the entire request.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The pytest tests do more than confirm that the endpoint returns HTTP 400.&lt;/p&gt;

&lt;p&gt;They also verify that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the contents of &lt;code&gt;DailySales&lt;/code&gt; are unchanged before and after the POST request.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product A: currently 5, requested update to 9
Product B: invalid data

↓ Reject the entire request

Product A: remains 5
Product B: nothing is saved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that partial updates do not occur.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. "The Product Exists" Wasn't Enough
&lt;/h2&gt;

&lt;p&gt;Next, I checked which products were allowed to receive sales entries.&lt;/p&gt;

&lt;p&gt;Even if a product ID is numerically valid, I don't want sales registered against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a product that does not exist&lt;/li&gt;
&lt;li&gt;a product belonging to a different year or month&lt;/li&gt;
&lt;li&gt;a discontinued product&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I added pytest cases that reject all of these with HTTP 400:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unknown product
Product from another month
Discontinued product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, even if valid products are submitted together with an invalid one, &lt;code&gt;DailySales&lt;/code&gt; must remain unchanged.&lt;/p&gt;

&lt;p&gt;If a discontinued product already has historical sales data, those records must also remain untouched.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Teaching the Database That There Can Be Only One Sales Record per Product and Date
&lt;/h2&gt;

&lt;p&gt;The application already had logic that worked like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If sales already exist for the same product and date, update the quantity instead of inserting another row.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, the database itself did not have a rule preventing duplicate rows for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same product
+
same date
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I added a unique constraint to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(product_id, date)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A unique constraint basically tells the database:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"This combination may exist only once."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This gives me two layers of protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application logic
+
Database constraint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In pytest, I bypass the Flask UI and attempt to insert two sales records with the same product and date directly into the test database.&lt;/p&gt;

&lt;p&gt;The test confirms that the database raises:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IntegrityError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;For duplicate sales on the same product and date, even if application-level logic is bypassed, the database itself now rejects the duplicate.&lt;/p&gt;

&lt;p&gt;Creating that second layer of protection was the goal here.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. Testing the Migration in an Isolated PostgreSQL Environment
&lt;/h2&gt;

&lt;p&gt;Because I changed the database schema, I also added an Alembic migration.&lt;/p&gt;

&lt;p&gt;I think of migrations as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a mechanism for recording database schema changes and applying them in a controlled order.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This migration added the unique constraint to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DailySales(product_id, date)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, I did not want to immediately test it against the PostgreSQL database I normally use.&lt;/p&gt;

&lt;p&gt;Instead, I created a separate Docker environment and isolated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the container&lt;/li&gt;
&lt;li&gt;the network&lt;/li&gt;
&lt;li&gt;the database&lt;/li&gt;
&lt;li&gt;the volume&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;upgrading from an empty database to the latest schema&lt;/li&gt;
&lt;li&gt;upgrading from a state similar to an existing database&lt;/li&gt;
&lt;li&gt;downgrading&lt;/li&gt;
&lt;li&gt;upgrading again&lt;/li&gt;
&lt;li&gt;confirming that existing record counts and contents remained unchanged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also attempted a duplicate INSERT and confirmed that PostgreSQL itself rejected it as a unique constraint violation.&lt;/p&gt;

&lt;p&gt;After the test, I removed the isolated containers and volumes.&lt;/p&gt;

&lt;p&gt;The idea was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;instead of trying something in the normal environment and recovering if it fails, test it first somewhere where failure cannot affect the normal environment.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Roll Back When Saving Fails
&lt;/h2&gt;

&lt;p&gt;The next area I checked was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;what happens when the input is valid, but the final database save fails?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When saving changes to the database, the application eventually calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In simple terms, this means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Make these changes permanent.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;commit()&lt;/code&gt; can fail too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sales POST
&lt;/h3&gt;

&lt;p&gt;Imagine this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product A: 5 → preparing to update to 9
Product B: preparing to insert 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the final &lt;code&gt;commit()&lt;/code&gt; fails.&lt;/p&gt;

&lt;p&gt;In that situation, the application needs to explicitly cancel the pending changes and restore the database session to a usable state.&lt;/p&gt;

&lt;p&gt;So I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In pytest, I intentionally make &lt;code&gt;commit()&lt;/code&gt; fail and then verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product A DailySales: still quantity=5
Product B DailySales: not inserted
DailySales count: unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Product POST
&lt;/h3&gt;

&lt;p&gt;I also intentionally make &lt;code&gt;commit()&lt;/code&gt; fail during product registration and updates.&lt;/p&gt;

&lt;p&gt;The tests verify that the following remain unchanged from before the POST request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;existing product name&lt;/li&gt;
&lt;li&gt;price&lt;/li&gt;
&lt;li&gt;active/inactive status&lt;/li&gt;
&lt;li&gt;newly added products&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DailySales&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I added regression tests not only for input validation, but also for failures during the final save operation, ensuring that incomplete changes are not left behind.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. Testing 17 Types of Invalid Product Input
&lt;/h2&gt;

&lt;p&gt;I didn't stop at sales input.&lt;/p&gt;

&lt;p&gt;I also reviewed the product registration and update form.&lt;/p&gt;

&lt;p&gt;Before the fix, there were insufficiently protected paths involving cases such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;zip()&lt;/code&gt; silently ignoring extra values when field counts did not match&lt;/li&gt;
&lt;li&gt;non-numeric product IDs&lt;/li&gt;
&lt;li&gt;product IDs that did not exist&lt;/li&gt;
&lt;li&gt;product IDs belonging to another month&lt;/li&gt;
&lt;li&gt;the same product ID being submitted multiple times&lt;/li&gt;
&lt;li&gt;invalid prices&lt;/li&gt;
&lt;li&gt;empty or non-numeric &lt;code&gt;year&lt;/code&gt; and &lt;code&gt;month&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;month=0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;month=13&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I added 17 invalid-input cases.&lt;/p&gt;

&lt;p&gt;Before the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;17 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;17 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, the important part is not only returning HTTP 400.&lt;/p&gt;

&lt;p&gt;The tests also confirm that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;both &lt;code&gt;Product&lt;/code&gt; and &lt;code&gt;DailySales&lt;/code&gt; remain unchanged before and after the POST request.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The new flow became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Validate everything
↓
Everything is valid
↓
Only then modify the database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also tested valid cases, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;updating an existing product&lt;/li&gt;
&lt;li&gt;adding a new product&lt;/li&gt;
&lt;li&gt;products priced at 0&lt;/li&gt;
&lt;li&gt;ensuring existing sales history is not modified&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Keep Historical Sales Even After a Product Is Discontinued
&lt;/h2&gt;

&lt;p&gt;In this application, discontinuing a product does not delete it from the database.&lt;/p&gt;

&lt;p&gt;Instead, I use a form of soft deletion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;is_active=True
↓
is_active=False
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a reason for this.&lt;/p&gt;

&lt;p&gt;If the product itself were deleted, handling historical sales linked to that product would become much more difficult.&lt;/p&gt;

&lt;p&gt;So I added pytest cases verifying that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the product row still exists&lt;/li&gt;
&lt;li&gt;the Product ID does not change&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_active=False&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;historical &lt;code&gt;DailySales&lt;/code&gt; remain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also tested what happens when an existing Product ID for a discontinued product is submitted again.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a new product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the application should do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Same Product ID
↓
is_active=True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests also verify that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the number of Product records does not increase&lt;/li&gt;
&lt;li&gt;historical &lt;code&gt;DailySales&lt;/code&gt; remain unchanged&lt;/li&gt;
&lt;li&gt;the same Product ID continues to be used&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two tests passed immediately when I added them.&lt;/p&gt;

&lt;p&gt;So in this case, I did not fix production code.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I recorded behavior that was already correct so that future changes cannot accidentally break it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Finally, Verifying Dashboard Aggregation
&lt;/h2&gt;

&lt;p&gt;At the end of Stage 2, I also tested the dashboard API.&lt;/p&gt;

&lt;p&gt;I inserted sales data into the database like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product A
3 + 7 = 10

Product B
5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I requested:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/dashboard-data?year=2026&amp;amp;month=8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and verified with pytest that the API returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product A: 10
Product B: 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also checked that the ordering and values of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ranked_sales
chart_labels
chart_values
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;matched correctly.&lt;/p&gt;

&lt;p&gt;Historical sales for discontinued products remain included in the aggregation.&lt;/p&gt;

&lt;p&gt;And when no year/month filter is provided, I also verify that sales across:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;all periods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are included in the aggregation.&lt;/p&gt;

&lt;p&gt;All four dashboard API tests passed immediately when I added them.&lt;/p&gt;

&lt;p&gt;Again, this was a case where:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the current implementation already behaved correctly, so I recorded that behavior as regression tests to prevent it from being broken later.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These API tests also verify that the Gemini client is not called.&lt;/p&gt;

&lt;p&gt;That lets me test the database aggregation logic independently from the external AI API.&lt;/p&gt;




&lt;h2&gt;
  
  
  From 9 Tests to 51
&lt;/h2&gt;

&lt;p&gt;At the end of Stage 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the end of Stage 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;51 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this does &lt;strong&gt;not&lt;/strong&gt; mean:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;51 tests = a safe application&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;pytest can only check what has actually been written as a test.&lt;/p&gt;

&lt;p&gt;During this work, I also discovered several areas where the specification was still unclear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how to aggregate different Product IDs that share the same product name&lt;/li&gt;
&lt;li&gt;maximum product-name length&lt;/li&gt;
&lt;li&gt;acceptable range for &lt;code&gt;year&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ranking order when products have identical quantities&lt;/li&gt;
&lt;li&gt;API behavior when there are zero sales&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not let Codex decide these specifications on its own and expand the scope of the current work.&lt;/p&gt;

&lt;p&gt;Instead, I left them as unresolved items for later.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The number of tests itself matters less than understanding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;what kind of incident each test is supposed to prevent.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I Learned from Stage 2
&lt;/h2&gt;

&lt;p&gt;I did not begin Stage 2 with a perfectly designed, large test plan.&lt;/p&gt;

&lt;p&gt;Instead, the process looked more like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If this case is covered, what about this one?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every time I checked one condition, another possible gap appeared.&lt;/p&gt;

&lt;p&gt;Then I added another test.&lt;/p&gt;

&lt;p&gt;Eventually, I ended up testing the whole flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input
↓
Product validation
↓
Database save
↓
Rollback on failure
↓
History preservation
↓
Aggregation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thinking about it through my work as a truck driver, it feels similar to moving beyond simply saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Be careful not to cause an accident."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, you build mechanisms such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;don't allow dangerous cargo to be loaded incorrectly&lt;/li&gt;
&lt;li&gt;don't let the vehicle depart if the load is unsafe&lt;/li&gt;
&lt;li&gt;stop the process if something goes wrong&lt;/li&gt;
&lt;li&gt;preserve past records&lt;/li&gt;
&lt;li&gt;check the final result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That feels much closer to what I'm trying to do with pytest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In Stage 2 of strengthening my pytest suite, I verified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full validation before processing sales POST requests&lt;/li&gt;
&lt;li&gt;product existence, year/month association, and active status&lt;/li&gt;
&lt;li&gt;a database unique constraint on &lt;code&gt;(product_id, date)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;an Alembic migration&lt;/li&gt;
&lt;li&gt;upgrade and downgrade testing in isolated PostgreSQL&lt;/li&gt;
&lt;li&gt;rollback behavior for sales POST requests&lt;/li&gt;
&lt;li&gt;17 invalid-input cases for product POST requests&lt;/li&gt;
&lt;li&gt;rollback behavior for product POST requests&lt;/li&gt;
&lt;li&gt;soft deletion and preservation of historical sales&lt;/li&gt;
&lt;li&gt;database aggregation in the dashboard API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full pytest suite grew from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9 passed
↓
51 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing became much clearer to me during this stage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instead of relying on people to remember not to repeat the same mistake, it is more effective to build a mechanism that automatically stops the system when the same dangerous state appears again.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the same time, there are still specifications I haven't decided and areas I haven't tested yet.&lt;/p&gt;

&lt;p&gt;In the next stage, I plan to look at authentication, authorization, CSRF protection, and questions such as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Who is actually allowed to perform this operation?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My work on growing pytest into an "incident prevention log" still has a little way to go.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Was Thinking About at the Time — on Zenn
&lt;/h2&gt;

&lt;p&gt;This article focused on the actual verification work I performed during Stage 2, including invalid input, rollback behavior, database constraints, and preserving historical data.&lt;/p&gt;

&lt;p&gt;On Zenn, I looked at the same stage from a slightly different angle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did I stop thinking "returning an error is enough" and start checking what the database looks like after the failure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is more of a reflection on how my thinking changed during development.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zenn.dev/tosane932/articles/abc04ddc9e74f4" rel="noopener noreferrer"&gt;https://zenn.dev/tosane932/articles/abc04ddc9e74f4&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/turning-pytest-into-an-incident-prevention-ledger-phase-1-from-3-simple-tests-to-9-2o53"&gt;Stage 1 — From 3 Simple Tests to 9&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-2-strengthening-sales-and-product-57b3"&gt;Stage 2 — Strengthening Sales and Product Registration&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-3-authentication-csrf-and-access-5gi2"&gt;Stage 3 — Authentication, CSRF, and Access Control&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-4-testing-failure-scenarios-71-87-12e7"&gt;Stage 4 — Testing Failure Scenarios&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-5-questioning-green-with-mutation-1e8a"&gt;Stage 5 — Questioning GREEN with Mutation Testing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932" rel="noopener noreferrer"&gt;https://qiita.com/tosane932&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>pytest</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Don't Let Codex Roam Free: 6 Guardrails I Use for AI-Assisted Coding</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Fri, 21 Aug 2026 08:01:14 +0000</pubDate>
      <link>https://dev.to/tosane932/dont-let-codex-roam-free-6-guardrails-i-use-for-ai-assisted-coding-4bj4</link>
      <guid>https://dev.to/tosane932/dont-let-codex-roam-free-6-guardrails-i-use-for-ai-assisted-coding-4bj4</guid>
      <description>&lt;h2&gt;
  
  
  Leaving Everything to AI Feels Risky. But Codex Became Much More Reliable Once I Put Up Some Guardrails
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I currently work as a truck driver while teaching myself web application development using Python and Flask.&lt;/p&gt;

&lt;p&gt;Recently, I've been using Codex in VS Code while working on my personal application, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;improving security&lt;/li&gt;
&lt;li&gt;strengthening pytest coverage&lt;/li&gt;
&lt;li&gt;validating database behavior&lt;/li&gt;
&lt;li&gt;checking migrations&lt;/li&gt;
&lt;li&gt;handling invalid input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I read about AI-assisted coding, I often come across warnings like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you leave everything to AI, things can go badly wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think that's true.&lt;/p&gt;

&lt;p&gt;But after using Codex for a while, I started to feel that there is a big difference between:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;letting AI roam free&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;defining its working area before assigning the task.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this article, when I say "guardrails," I mean deciding not only &lt;strong&gt;what Codex should do&lt;/strong&gt;, but also &lt;strong&gt;what it must not do and where it should stop&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm still a beginner in programming, but these are the six guardrails I've started using when working with Codex.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Define What It Is Allowed to Touch
&lt;/h2&gt;

&lt;p&gt;The first thing I do is limit the scope of the task.&lt;/p&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What exactly are we changing this time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only validate input for the sales POST endpoint&lt;/li&gt;
&lt;li&gt;only handle rollback when a database save fails&lt;/li&gt;
&lt;li&gt;only validate POST requests for the product master&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I try to keep each task focused on one small theme.&lt;/p&gt;

&lt;p&gt;I also tell Codex:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you find another problem outside the current scope, do not fix it automatically. Report it instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is nothing wrong with Codex discovering another issue while investigating.&lt;/p&gt;

&lt;p&gt;But if it keeps expanding the task and fixing additional problems along the way, I can eventually end up wondering:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What exactly changed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I try to separate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;discovering a problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;fixing a problem.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Create a Failing Test First
&lt;/h2&gt;

&lt;p&gt;Recently, I've increasingly asked Codex to write pytest tests before modifying the production code.&lt;/p&gt;

&lt;p&gt;My basic workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reproduce the dangerous condition with a test&lt;/li&gt;
&lt;li&gt;Run the test before making the fix&lt;/li&gt;
&lt;li&gt;Confirm that the test fails&lt;/li&gt;
&lt;li&gt;Make the smallest necessary change&lt;/li&gt;
&lt;li&gt;Confirm that the target test passes&lt;/li&gt;
&lt;li&gt;Run the entire pytest suite again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, when I strengthened input validation for my product master, I added 17 invalid-input cases.&lt;/p&gt;

&lt;p&gt;Before the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;17 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;17 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;By confirming that the test fails first, I can verify more than just:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It seems safer now."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I can confirm that the problem actually existed, that the test reproduced it, and that the change closed the gap.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recently, I've stopped thinking of pytest as just a way to confirm that the application works.&lt;/p&gt;

&lt;p&gt;Instead, I think of it more like an:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;incident-prevention log.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once I discover a dangerous condition, I record it as a test so the application cannot silently return to that state later.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Don't Let It Directly Touch the Normal Database or Production
&lt;/h2&gt;

&lt;p&gt;During testing, I generally avoid connecting Codex-driven tests directly to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the PostgreSQL database used during normal development&lt;/li&gt;
&lt;li&gt;the normal Docker database&lt;/li&gt;
&lt;li&gt;the actual Gemini API&lt;/li&gt;
&lt;li&gt;the production environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For ordinary pytest runs, I separate the test database from the normal database and use a disposable in-memory SQLite database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sqlite:///:memory:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For regular tests, this gives me a fast and disposable environment.&lt;/p&gt;

&lt;p&gt;However, I do &lt;strong&gt;not&lt;/strong&gt; assume SQLite can fully reproduce PostgreSQL-specific behavior.&lt;/p&gt;

&lt;p&gt;When I need to verify PostgreSQL-specific behavior or migrations, I create a separate isolated PostgreSQL environment.&lt;/p&gt;

&lt;p&gt;I separate things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;containers&lt;/li&gt;
&lt;li&gt;networks&lt;/li&gt;
&lt;li&gt;databases&lt;/li&gt;
&lt;li&gt;volumes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;from the normal environment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My thinking is not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It's only a test, so it's probably fine."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Test in a place where failure cannot easily spread into the normal environment."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal is not simply to avoid using the production or normal database.&lt;/p&gt;

&lt;p&gt;The real goal is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;to reduce the impact if something goes wrong.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Don't Let an Unexpected Problem Expand the Task Automatically
&lt;/h2&gt;

&lt;p&gt;While Codex is investigating one issue, it sometimes discovers another unrelated problem.&lt;/p&gt;

&lt;p&gt;In the past, I might have said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Go ahead and fix that too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now I usually stop there.&lt;/p&gt;

&lt;p&gt;I increasingly tell Codex:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you discover an unexpected problem, do not expand the current scope. Stop and report it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current task:
Validate the sales POST endpoint
        ↓
Codex discovers another database issue
        ↓
Do not fix it immediately
        ↓
Report it as an unresolved issue
        ↓
Create a separate task for it later
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents a single change from becoming unnecessarily large.&lt;/p&gt;

&lt;p&gt;For me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;being able to trace what changed is more important than fixing every discovered problem immediately.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Keep a Human Check Before Commit and Push
&lt;/h2&gt;

&lt;p&gt;Even after the code changes are complete and the tests pass, I don't immediately let Codex commit or push.&lt;/p&gt;

&lt;p&gt;First, I have it check things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest -v
git diff --check
git status --short
git diff --stat
git diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I review the results and check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Were only the intended files modified?&lt;/li&gt;
&lt;li&gt;Did all pytest tests pass?&lt;/li&gt;
&lt;li&gt;Did unrelated changes get mixed in?&lt;/li&gt;
&lt;li&gt;Were any unexpected files added?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after that do I give permission:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Everything looks good up to this point. You can commit."&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Codex can write code for me, but I don't want it to automatically pass through the final Git gate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After the commit, I also ask it to report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;commit hash&lt;/li&gt;
&lt;li&gt;commit message&lt;/li&gt;
&lt;li&gt;push destination&lt;/li&gt;
&lt;li&gt;push result&lt;/li&gt;
&lt;li&gt;final &lt;code&gt;git status&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recently, I've also started thinking that relying only on a written instruction like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Do not push."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;is not strong enough.&lt;/p&gt;

&lt;p&gt;If I forget to include that instruction even once, the AI may interpret pushing as allowed.&lt;/p&gt;

&lt;p&gt;So ideally, I want two layers of protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Restrict the action in the prompt
+
Restrict the actual permissions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If I also want to account for human mistakes, simply saying:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Don't do this."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;is weaker than creating an environment where the action is difficult or impossible without explicit permission.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. Let pytest and CI Inspect the Result Too
&lt;/h2&gt;

&lt;p&gt;After pytest passes locally, the relevant pushes and Pull Requests trigger another pytest run through GitHub Actions.&lt;/p&gt;

&lt;p&gt;In my current setup, GitHub Actions runs when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code is pushed to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a Pull Request targeting &lt;code&gt;main&lt;/code&gt; is created&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In another article, I compared this process to truck inspections.&lt;/p&gt;

&lt;p&gt;For me, the analogy looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pytest:&lt;/strong&gt; the inspection tool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pytest tests:&lt;/strong&gt; the inspection checklist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;local pytest:&lt;/strong&gt; the pre-departure inspection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions:&lt;/strong&gt; the automated inspection at the shipping gate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The task isn't finished just because Codex says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The fix is complete!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, I try to make every change pass through several checkpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Codex
  ↓
pytest
  ↓
git diff
  ↓
commit
  ↓
push
  ↓
GitHub Actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I don't use the AI's own response as the final source of truth.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I use other mechanisms to verify the work performed by the AI.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Scares Me About Letting Codex "Roam Free"?
&lt;/h2&gt;

&lt;p&gt;Imagine giving Codex a prompt like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make this application secure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's all.&lt;/p&gt;

&lt;p&gt;Now the AI has to guess many things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much is it allowed to change?&lt;/li&gt;
&lt;li&gt;Can it modify the database?&lt;/li&gt;
&lt;li&gt;Can it connect to production?&lt;/li&gt;
&lt;li&gt;Should it fix additional problems it discovers?&lt;/li&gt;
&lt;li&gt;Can it commit?&lt;/li&gt;
&lt;li&gt;Can it push?&lt;/li&gt;
&lt;li&gt;Can it change the specification?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a lot of decisions.&lt;/p&gt;

&lt;p&gt;What worries me isn't Codex itself.&lt;/p&gt;

&lt;p&gt;What worries me more is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the human defining nothing and then delegating not only the work, but also all of the decisions to the AI.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Delegating work to AI&lt;/strong&gt; and &lt;strong&gt;dumping everything on AI&lt;/strong&gt; may look similar, but I think they are very different.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  It Reminded Me of My Work as a Truck Driver
&lt;/h2&gt;

&lt;p&gt;When I thought about it, this approach felt surprisingly similar to my main job as a truck driver.&lt;/p&gt;

&lt;p&gt;Normally, you wouldn't tell a new driver:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Just deliver this cargo somehow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You would confirm things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where they are going&lt;/li&gt;
&lt;li&gt;which route they should take&lt;/li&gt;
&lt;li&gt;the delivery time&lt;/li&gt;
&lt;li&gt;what they need to watch out for&lt;/li&gt;
&lt;li&gt;what they must not do&lt;/li&gt;
&lt;li&gt;what to do if something unexpected happens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And when something unusual occurs, there are situations where the right action is not to make an arbitrary decision and continue.&lt;/p&gt;

&lt;p&gt;Sometimes the correct action is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;stop and confirm.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While working with Codex, I started thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is actually similar to assigning work to someone in the real world.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Don't Just Give AI a Task — Define the Area Where It Can Work
&lt;/h2&gt;

&lt;p&gt;I'm still relatively new to programming.&lt;/p&gt;

&lt;p&gt;That means I cannot always evaluate 100% of Codex's output by myself.&lt;/p&gt;

&lt;p&gt;And that's exactly why I don't want to completely trust the AI and give it unlimited freedom.&lt;/p&gt;

&lt;p&gt;Instead, I try to create an environment where:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;even if something goes wrong, the damage is less likely to spread.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same idea applies to the pytest improvements I've been working on.&lt;/p&gt;

&lt;p&gt;My goal is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Be careful not to cause the same bug again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My goal is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the application ever returns to the same dangerous state, pytest should stop it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I think the same way about Codex.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I don't let Codex roam free.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define the scope&lt;/li&gt;
&lt;li&gt;Create failing tests first&lt;/li&gt;
&lt;li&gt;Keep it away from normal databases and production&lt;/li&gt;
&lt;li&gt;Make it stop when something unexpected appears&lt;/li&gt;
&lt;li&gt;Review changes before commit and push&lt;/li&gt;
&lt;li&gt;Inspect the result again with pytest and CI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I create those guardrails first, and then I let the AI work inside them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And ideally, I don't want those guardrails to exist only in the prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where possible, I also want to enforce them through actual permissions and environment restrictions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's something I plan to pay more attention to going forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Letting Codex roam free still scares me.&lt;br&gt;&lt;br&gt;
But with clear guardrails, I've found it can be extremely useful.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's what I've learned through my recent personal development work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932" rel="noopener noreferrer"&gt;https://qiita.com/tosane932&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codex</category>
      <category>pytest</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>🔰 pytest, GitHub Actions, and CI — Explained Through a Truck Driver’s Inspection Log</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Mon, 17 Aug 2026 12:12:00 +0000</pubDate>
      <link>https://dev.to/tosane932/pytest-github-actions-and-ci-explained-through-a-truck-drivers-inspection-log-4coa</link>
      <guid>https://dev.to/tosane932/pytest-github-actions-and-ci-explained-through-a-truck-drivers-inspection-log-4coa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article records how I understood the relationship between pytest, GitHub Actions, and CI at the point when I had expanded my test suite from 3 tests to 9.&lt;/p&gt;

&lt;p&gt;Since then, I have continued developing pytest as what I call an &lt;strong&gt;“incident-prevention ledger.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The regression suite has expanded into areas such as sales input, product registration, database constraints, rollback behavior, historical-data preservation, and dashboard aggregation.&lt;/p&gt;

&lt;p&gt;Therefore, references to “9 tests” and “areas I want to test next” describe the state of the project &lt;strong&gt;at the time this article was written&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hello from Japan! 🇯🇵&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I currently work as a truck driver while teaching myself web application development with Python and Flask.&lt;/p&gt;

&lt;p&gt;At the time I wrote this article, my total learning time had reached &lt;strong&gt;159 hours&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my personal project, I had been working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XSS protection&lt;/li&gt;
&lt;li&gt;Database migration repairs&lt;/li&gt;
&lt;li&gt;Strengthening pytest&lt;/li&gt;
&lt;li&gt;Automated testing with GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But because I was still a beginner, I kept running into very basic questions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What exactly is pytest doing?&lt;/p&gt;

&lt;p&gt;What is the difference between pytest and GitHub Actions?&lt;/p&gt;

&lt;p&gt;And where did this thing called CI suddenly come from?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Rather than memorizing the terminology, I tried to understand it using concepts from my day job:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Truck inspection logs and workplace safety management.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is my beginner-level explanation of pytest, GitHub Actions, CI, and regression testing through that analogy.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Was Actually Building
&lt;/h2&gt;

&lt;p&gt;At the time, my development workflow was starting to look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Discover a bug or near miss
        ↓
Investigate the cause
        ↓
Fix the code
        ↓
Add a regression test to pytest
        ↓
Push to GitHub
        ↓
GitHub Actions automatically runs pytest
        ↓
If the dangerous state returns, CI fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, I thought I was simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Writing pytest tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But gradually, I realized I was building something more useful:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A system that records previously discovered problems and automatically warns me if the application returns to the same dangerous state.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Is pytest?
&lt;/h2&gt;

&lt;p&gt;pytest is a tool for writing and running tests in Python.&lt;/p&gt;

&lt;p&gt;Very simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It automatically checks whether your program still behaves the way you expect.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, imagine this function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;means something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I expect this to be true.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the result is correct, the test passes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the result is wrong, it fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the basic idea.&lt;/p&gt;




&lt;h2&gt;
  
  
  In Trucking Terms, a pytest Test Is an Inspection Item
&lt;/h2&gt;

&lt;p&gt;Before taking a truck onto the road, there are things that must be checked.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;□ Are the tires in good condition?
□ Do the lights work?
□ Is the oil level okay?
□ Are the brakes working normally?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pytest started making more sense to me when I thought about it the same way.&lt;/p&gt;

&lt;p&gt;For a software system, the inspection items might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;□ Is the AI prompt still structured correctly?
□ Is untrusted text still rendered safely?
□ Is product data processed correctly?
□ Does invalid sales input leave the database unchanged?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in my head:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One pytest test ≈ one inspection item.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That simple analogy made the concept much easier to understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Test Suite Feels Like an Inspection Logbook
&lt;/h2&gt;

&lt;p&gt;When multiple pytest tests are collected together, they start to resemble an inspection logbook.&lt;/p&gt;

&lt;p&gt;At the time this article was written, I had expanded the project from 3 pytest tests to 9.&lt;/p&gt;

&lt;p&gt;But I was not simply trying to increase the number.&lt;/p&gt;

&lt;p&gt;For example, I had previously discovered a place where AI-generated responses were displayed using JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After fixing that XSS risk, I added a pytest regression test to check:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Has this rendering path accidentally returned to the dangerous implementation?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The workflow became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Discover an XSS risk
        ↓
Fix it
        ↓
Record the fix as a pytest regression test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if I forget the details six months later, pytest can still remember the inspection item for me.&lt;/p&gt;

&lt;p&gt;I have continued expanding the test suite using this idea.&lt;/p&gt;




&lt;h2&gt;
  
  
  “I'll Be More Careful Next Time” Is Not Enough
&lt;/h2&gt;

&lt;p&gt;This is another idea that comes from my day job.&lt;/p&gt;

&lt;p&gt;After an accident or mistake, saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I'll be more careful next time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is weak as a prevention strategy.&lt;/p&gt;

&lt;p&gt;It still depends entirely on human attention.&lt;/p&gt;

&lt;p&gt;I think an important distinction is between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never make the same mistake again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prevent the same dangerous situation from existing again.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Suppose there is a location on a delivery route where a dangerous situation repeatedly occurs at the same time of day.&lt;/p&gt;

&lt;p&gt;One response is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Drive more carefully there.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But stronger options might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Change the delivery time
Change the delivery order
Use another route
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of relying only on attention, change the conditions that create the risk.&lt;/p&gt;

&lt;p&gt;I found the same idea useful in software development.&lt;/p&gt;




&lt;h2&gt;
  
  
  XSS Is a Good Example
&lt;/h2&gt;

&lt;p&gt;My application previously displayed AI-generated text using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A weak response would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Be careful not to pass dangerous HTML into it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, I changed the implementation so that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Even if HTML-like text arrives,
it is not interpreted as HTML.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I added a pytest regression test that checks whether the relevant code path has returned to its previous dangerous form.&lt;/p&gt;

&lt;p&gt;This changes the strategy from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Be careful.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Build the system so that the unsafe state is harder to recreate.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That feels very similar to workplace safety management.&lt;/p&gt;




&lt;h2&gt;
  
  
  So What Is GitHub Actions?
&lt;/h2&gt;

&lt;p&gt;This is where I originally became confused.&lt;/p&gt;

&lt;p&gt;To run pytest locally, I type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if pytest already runs the tests, what does GitHub Actions do?&lt;/p&gt;

&lt;p&gt;In my repository, GitHub Actions is configured so that when I push code to GitHub:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;GitHub automatically prepares an environment and runs pytest for me.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can trigger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Actions starts
        ↓
Prepare a Python environment
        ↓
Install dependencies
        ↓
Run pytest -v
        ↓
Report the result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my current configuration, tests also run for Pull Requests targeting &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So pytest is the testing tool.&lt;/p&gt;

&lt;p&gt;GitHub Actions is the automation platform that runs those tests in response to GitHub events.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre-Trip Inspection and the Shipping Gate
&lt;/h2&gt;

&lt;p&gt;The trucking analogy helped here too.&lt;/p&gt;

&lt;p&gt;When I run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;on my own machine, I think of it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A pre-trip inspection I perform myself.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I push the code.&lt;/p&gt;

&lt;p&gt;GitHub Actions runs pytest again in GitHub's environment.&lt;/p&gt;

&lt;p&gt;I think of that as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Another automated inspection at the shipping gate.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer:
"I inspected it locally."
        ↓
push
        ↓
GitHub Actions:
"We'll inspect it here too."
        ↓
Tests passed
        ↓
OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful part is that the code is checked not only in my own development environment, but also in another automated environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then What Is CI?
&lt;/h2&gt;

&lt;p&gt;This brings us to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;which stands for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous Integration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I first heard that term, I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Okay... but what does it actually do?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For my current level and project, the easiest way to understand CI is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A workflow that automatically checks code changes when they are integrated into the shared repository.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CI can include more than testing.&lt;/p&gt;

&lt;p&gt;For example, it may also include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linting&lt;/li&gt;
&lt;li&gt;Static analysis&lt;/li&gt;
&lt;li&gt;Builds&lt;/li&gt;
&lt;li&gt;Security checks&lt;/li&gt;
&lt;li&gt;Formatting checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But in my repository, the central CI activity is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Actions automatically running pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I stopped trying to understand CI as an abstract buzzword and instead connected it to what my project was actually doing.&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest vs. GitHub Actions vs. CI
&lt;/h2&gt;

&lt;p&gt;This is how I currently organize the concepts in my head:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;My Understanding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pytest&lt;/td&gt;
&lt;td&gt;The inspection tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One pytest test&lt;/td&gt;
&lt;td&gt;One inspection item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test files&lt;/td&gt;
&lt;td&gt;The inspection logbook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;The equipment that runs inspections automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;The ongoing process of automatically checking code changes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once I separated the responsibilities this way, everything became much easier to understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Actual GitHub Actions Configuration
&lt;/h2&gt;

&lt;p&gt;The important part of my workflow looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest -v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A push to &lt;code&gt;main&lt;/code&gt; or a Pull Request targeting &lt;code&gt;main&lt;/code&gt; causes GitHub Actions to run pytest automatically.&lt;/p&gt;

&lt;p&gt;Previously, the workflow used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;pytest test_prompts.py -v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That only executed one specific test file.&lt;/p&gt;

&lt;p&gt;So even if I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_security.py
test_sales.py
test_database.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Actions would ignore them.&lt;/p&gt;

&lt;p&gt;I changed it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;pytest -v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now pytest uses its normal discovery rules and automatically includes newly added tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Is This Useful?
&lt;/h2&gt;

&lt;p&gt;Imagine I change a completely different feature in the future.&lt;/p&gt;

&lt;p&gt;The new feature itself may work correctly.&lt;/p&gt;

&lt;p&gt;But without realizing it, I could accidentally break an XSS protection that I fixed months earlier.&lt;/p&gt;

&lt;p&gt;It would be difficult for a human to remember every previous issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check XSS
Check prompts
Check Jinja
Check Gemini
Check sales behavior
Check database state
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;every single time.&lt;/p&gt;

&lt;p&gt;Instead, I can record those checks in pytest.&lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest performs the inspections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for me.&lt;/p&gt;

&lt;p&gt;And with GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Even if I forget to run pytest locally,
GitHub performs another automated check.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; mean GitHub Actions makes local testing unnecessary.&lt;/p&gt;

&lt;p&gt;My mental model is still:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Local pytest = pre-trip inspection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions = automated inspection at the shipping gate&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two layers are better than relying on one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Turning Small Near Misses into Tests
&lt;/h2&gt;

&lt;p&gt;At the time I wrote this article, I wanted to continue this cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Discover a small bug
        ↓
Investigate the cause
        ↓
Fix it
        ↓
Write a regression test
        ↓
Let GitHub Actions check it every time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why I started calling pytest:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An incident-prevention ledger.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every time I discover another problem, another inspection item can be added to the logbook.&lt;/p&gt;

&lt;p&gt;Since then, I have expanded the idea into database-changing operations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sales entry&lt;/li&gt;
&lt;li&gt;Product registration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those tests, I no longer want to check only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did the HTTP request return the expected response?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also want to check:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happened to the database after the request failed?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much stronger inspection.&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Alone Does Not Make a System Safe
&lt;/h2&gt;

&lt;p&gt;This is important.&lt;/p&gt;

&lt;p&gt;At the time this article was written, I had 9 pytest tests.&lt;/p&gt;

&lt;p&gt;That did &lt;strong&gt;not&lt;/strong&gt; mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This application is now completely safe!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many areas were still untested.&lt;/p&gt;

&lt;p&gt;At the time, I wanted to expand coverage into areas such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL migrations&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;CSRF&lt;/li&gt;
&lt;li&gt;Invalid sales input&lt;/li&gt;
&lt;li&gt;Gemini API failures&lt;/li&gt;
&lt;li&gt;Real browser DOM behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since then, I have added regression coverage for areas including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Invalid sales input&lt;/li&gt;
&lt;li&gt;Product registration&lt;/li&gt;
&lt;li&gt;Database uniqueness constraints&lt;/li&gt;
&lt;li&gt;Rollback behavior&lt;/li&gt;
&lt;li&gt;Historical-data preservation after soft deletion&lt;/li&gt;
&lt;li&gt;Dashboard aggregation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some other areas, particularly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;li&gt;CSRF&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;are still planned for separate stages.&lt;/p&gt;

&lt;p&gt;Having an inspection logbook does not guarantee that an accident can never happen.&lt;/p&gt;

&lt;p&gt;But it does make it harder to overlook a dangerous state that has already been discovered once.&lt;/p&gt;

&lt;p&gt;That alone is valuable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If someone had told me when I first started programming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest
GitHub Actions
CI
regression tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I probably would not have understood much.&lt;/p&gt;

&lt;p&gt;But translating those concepts into my own work experience made them much clearer.&lt;/p&gt;

&lt;p&gt;My current mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest
= inspection

pytest tests
= inspection items

test files
= inspection logbook

GitHub Actions
= automated inspection equipment

CI
= a process that continuously checks code changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My development workflow has gradually become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Discover a problem
        ↓
Investigate the cause
        ↓
Fix it
        ↓
Add a regression rule to pytest
        ↓
Verify locally
        ↓
Push
        ↓
GitHub Actions performs another automated inspection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the time I wrote this article, the suite contained 9 tests.&lt;/p&gt;

&lt;p&gt;Since then, I have continued adding real bugs and specifications that I want to protect.&lt;/p&gt;

&lt;p&gt;My goal is for pytest to become not merely:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A tool for checking whether things work&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;but:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An incident-prevention ledger containing lessons from previous bugs and near misses.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I am still learning.&lt;/p&gt;

&lt;p&gt;But I have found that understanding technical terminology does not always mean memorizing definitions.&lt;/p&gt;

&lt;p&gt;Sometimes the most useful question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is this actually doing?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then translating that answer into words I already understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  GitHub
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>pytest</category>
      <category>githubactions</category>
      <category>beginners</category>
    </item>
    <item>
      <title>📝 Turning pytest into an “Incident-Prevention Ledger” — Phase 1: From 3 Simple Tests to 9 Regression Tests</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:21:26 +0000</pubDate>
      <link>https://dev.to/tosane932/turning-pytest-into-an-incident-prevention-ledger-phase-1-from-3-simple-tests-to-9-2o53</link>
      <guid>https://dev.to/tosane932/turning-pytest-into-an-incident-prevention-ledger-phase-1-from-3-simple-tests-to-9-2o53</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article records &lt;strong&gt;Phase 1&lt;/strong&gt;, when I expanded the pytest suite from 3 tests to 9.&lt;/p&gt;

&lt;p&gt;At that time, AI responses were rendered using a combination of &lt;code&gt;createTextNode()&lt;/code&gt; and &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; elements.&lt;/p&gt;

&lt;p&gt;After publication, a reader suggested using &lt;code&gt;innerText&lt;/code&gt;. I tested that approach in my own environment and confirmed that, for this use case, it preserved line breaks while keeping HTML-like strings from being interpreted as HTML. The current implementation therefore uses &lt;code&gt;innerText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The XSS regression tests have also been updated to match the current implementation. They now verify that &lt;code&gt;innerText&lt;/code&gt; remains in use and that dangerous HTML sinks such as &lt;code&gt;innerHTML&lt;/code&gt; have not returned to the relevant rendering paths.&lt;/p&gt;

&lt;p&gt;For that reason, references to &lt;code&gt;createTextNode()&lt;/code&gt; and &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; in this article should be understood as a record of the implementation at the time &lt;strong&gt;Phase 1&lt;/strong&gt; was completed.&lt;/p&gt;

&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932/items/f3de1e190873a90de39f" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/f3de1e190873a90de39f&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932/items/b91261e7103df5792f7d" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/b91261e7103df5792f7d&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932/items/6d1ca5490979c8cf9d62" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/6d1ca5490979c8cf9d62&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932/items/372270330e73583a227f" rel="noopener noreferrer"&gt;https://qiita.com/tosane932/items/372270330e73583a227f&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hello from Japan! 🇯🇵&lt;/p&gt;

&lt;p&gt;I have been reviewing the security and maintainability of a Flask application I am developing, with help from Codex.&lt;/p&gt;

&lt;p&gt;So far, I have fixed issues such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XSS risks in AI response rendering&lt;/li&gt;
&lt;li&gt;Stored XSS in dynamically generated product rankings&lt;/li&gt;
&lt;li&gt;A broken migration history that could not build the application from an empty PostgreSQL database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While working through those fixes, one question started bothering me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Is my current pytest suite far too simple?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the time, I had only three pytest tests.&lt;/p&gt;

&lt;p&gt;All three focused on a single function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;build_sales_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I decided to change how I thought about pytest.&lt;/p&gt;

&lt;p&gt;Instead of treating it only as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A tool for checking whether the application currently works&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted to develop it into:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An incident-prevention ledger that records bugs, vulnerabilities, and near misses so the application cannot silently return to the same dangerous state later.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For Phase 1, I added or strengthened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full pytest discovery in GitHub Actions&lt;/li&gt;
&lt;li&gt;XSS regression tests&lt;/li&gt;
&lt;li&gt;Jinja autoescape regression tests&lt;/li&gt;
&lt;li&gt;Prompt contract tests&lt;/li&gt;
&lt;li&gt;A test confirming that the complete prompt is actually sent to Gemini&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Original pytest Suite
&lt;/h2&gt;

&lt;p&gt;Before this improvement, only three tests existed in &lt;code&gt;test_prompts.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They mainly checked that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sales data appeared somewhere in the prompt&lt;/li&gt;
&lt;li&gt;The phrase requesting three suggestions existed&lt;/li&gt;
&lt;li&gt;The return value was a string&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first glance, that sounds like a test suite.&lt;/p&gt;

&lt;p&gt;But after asking Codex to review the tests statically, I realized how weak they were.&lt;/p&gt;

&lt;p&gt;In an extreme case, an implementation like this could potentially still pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sales_summary&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; Give me three suggestions. Three bullet points.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That would still satisfy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The input sales data exists&lt;/li&gt;
&lt;li&gt;A fixed phrase exists&lt;/li&gt;
&lt;li&gt;The return value is a string&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But important parts of the real prompt could disappear, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The AI's role&lt;/li&gt;
&lt;li&gt;Analysis perspectives&lt;/li&gt;
&lt;li&gt;Number of recommendations&lt;/li&gt;
&lt;li&gt;Response format&lt;/li&gt;
&lt;li&gt;Expected answer length&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and the tests might still pass.&lt;/p&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tests passing did not necessarily mean the intended specification was protected.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Another Problem: CI Was Not Running Every Test
&lt;/h2&gt;

&lt;p&gt;During the investigation, I also found a problem in GitHub Actions.&lt;/p&gt;

&lt;p&gt;The CI workflow was running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;pytest test_prompts.py -v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That meant if I later added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_security.py
test_sales.py
test_api.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Actions would not run them.&lt;/p&gt;

&lt;p&gt;They might pass locally but be completely ignored by CI.&lt;/p&gt;

&lt;p&gt;That is a serious problem if pytest is supposed to become an incident-prevention system.&lt;/p&gt;

&lt;p&gt;I changed the command to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;pytest -v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets pytest use its normal test-discovery rules and automatically collect newly added test files.&lt;/p&gt;

&lt;p&gt;The actual change was only one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- pytest test_prompts.py -v
&lt;/span&gt;&lt;span class="gi"&gt;+ pytest -v
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But for the future of the test suite, that one line was important.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Goal of Phase 1
&lt;/h2&gt;

&lt;p&gt;The goal was &lt;strong&gt;not&lt;/strong&gt; simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Increase the number of tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, I wanted this behavior:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a bug or vulnerability that I have already fixed returns in the future, pytest should fail immediately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this phase, I deliberately avoided changing application behavior.&lt;/p&gt;

&lt;p&gt;I focused on converting already-fixed behavior into regression tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Growing the Suite from 3 Tests to 9
&lt;/h2&gt;

&lt;p&gt;By the end of Phase 1, the suite had grown from three tests to nine.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Verify That Sales Data Appears in the Correct Section
&lt;/h2&gt;

&lt;p&gt;The first strengthened test was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_build_sales_prompt_places_sales_data_in_its_section
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Previously, the test only checked whether the sales data appeared somewhere in the prompt.&lt;/p&gt;

&lt;p&gt;The new test checks that it appears in the &lt;strong&gt;correct sales-data section&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This makes it harder for a broken prompt structure to pass unnoticed.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Protect the AI Role and Analysis Contract
&lt;/h2&gt;

&lt;p&gt;The next test was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_build_sales_prompt_preserves_role_and_analysis_contract
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt contains important instructions defining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The AI's role&lt;/li&gt;
&lt;li&gt;The perspectives the AI should use when analyzing the sales data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Previously, a few surviving fixed strings could be enough for the tests to pass.&lt;/p&gt;

&lt;p&gt;Now, the test protects the broader analytical contract of the prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Protect the Output Contract
&lt;/h2&gt;

&lt;p&gt;I also added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_build_sales_prompt_preserves_output_contract
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This verifies important response requirements such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of recommendations&lt;/li&gt;
&lt;li&gt;Bullet-point formatting&lt;/li&gt;
&lt;li&gt;Expected answer length&lt;/li&gt;
&lt;li&gt;Conciseness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I intentionally did &lt;strong&gt;not&lt;/strong&gt; compare the entire prompt character-for-character.&lt;/p&gt;

&lt;p&gt;A full exact-string comparison would make the test excessively fragile.&lt;/p&gt;

&lt;p&gt;Even a harmless wording improvement could break it.&lt;/p&gt;

&lt;p&gt;Instead, I test the important pieces of the contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  Does the Complete Prompt Actually Reach Gemini?
&lt;/h2&gt;

&lt;p&gt;One test I considered especially important was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_generate_ai_advice_sends_complete_sales_prompt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Previously, I only tested:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;build_sales_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in isolation.&lt;/p&gt;

&lt;p&gt;But even if that function works perfectly, it is useless if the actual Gemini request does not use the prompt it creates.&lt;/p&gt;

&lt;p&gt;So the new test verifies that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The content generated by &lt;code&gt;build_sales_prompt()&lt;/code&gt; is actually passed to Gemini&lt;/li&gt;
&lt;li&gt;The configured model name is used&lt;/li&gt;
&lt;li&gt;Gemini's returned text is passed back correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This connects the unit-level prompt logic to the actual AI integration path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mocking Gemini Instead of Calling the Real API
&lt;/h2&gt;

&lt;p&gt;I did not want pytest to call the real Gemini API every time the suite ran.&lt;/p&gt;

&lt;p&gt;That would introduce unnecessary problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real API keys would be required&lt;/li&gt;
&lt;li&gt;External network access would be required&lt;/li&gt;
&lt;li&gt;Tests could consume API quota&lt;/li&gt;
&lt;li&gt;Results could depend on network availability&lt;/li&gt;
&lt;li&gt;External service failures could make local tests fail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to mock the Gemini client.&lt;/p&gt;

&lt;p&gt;I configured a dummy API key and replaced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with a mock.&lt;/p&gt;

&lt;p&gt;The response was also fixed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;SimpleNamespace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mocked AI advice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This created a test with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No real API key&lt;/li&gt;
&lt;li&gt;No external communication&lt;/li&gt;
&lt;li&gt;No real Gemini request&lt;/li&gt;
&lt;li&gt;No dependency on network conditions&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Regression Tests for XSS
&lt;/h2&gt;

&lt;p&gt;One of the most important goals of this phase was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Preserve the XSS fixes I had recently made.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_dynamic_ranking_product_name_uses_text_dom_api
test_dashboard_ai_responses_use_text_dom_api
test_input_ai_response_uses_text_dom_api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Stored XSS in the Dynamic Ranking
&lt;/h2&gt;

&lt;p&gt;Previously, dynamically displayed product names were inserted using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had already changed that code to use safe DOM APIs and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regression test protects that specific rendering path so that a future refactor does not accidentally return product names to a dangerous HTML sink.&lt;/p&gt;




&lt;h2&gt;
  
  
  XSS in AI Response Rendering
&lt;/h2&gt;

&lt;p&gt;AI business advice and AI greetings had also previously used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the time &lt;strong&gt;Phase 1&lt;/strong&gt; was implemented, I had replaced that with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createTextNode()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DOM APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;so that the AI response itself would never be interpreted as HTML.&lt;/p&gt;

&lt;p&gt;I added source guards to ensure that those rendering paths did not silently return to &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After publishing the earlier article, I tested and adopted &lt;code&gt;innerText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The regression tests have since been updated to match the current implementation.&lt;/p&gt;

&lt;p&gt;They now verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;innerText&lt;/code&gt; remains in the relevant rendering path&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;innerHTML&lt;/code&gt; does not return there&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outerHTML&lt;/code&gt; does not return there&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;insertAdjacentHTML&lt;/code&gt; does not return there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation changed.&lt;/p&gt;

&lt;p&gt;The protected security property did not.&lt;/p&gt;




&lt;h2&gt;
  
  
  I Did Not Ban the Word &lt;code&gt;innerHTML&lt;/code&gt; Everywhere
&lt;/h2&gt;

&lt;p&gt;I thought carefully about this.&lt;/p&gt;

&lt;p&gt;It would be very easy to write a test like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fail if the string "innerHTML" appears anywhere in the file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that would be too broad.&lt;/p&gt;

&lt;p&gt;A future feature might have a legitimate and safely controlled reason to use &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A global ban could create false failures unrelated to the original vulnerability.&lt;/p&gt;

&lt;p&gt;So I limited the regression tests to the actual external-string rendering paths that previously caused problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product rankings&lt;/li&gt;
&lt;li&gt;AI business advice&lt;/li&gt;
&lt;li&gt;AI greetings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal of pytest is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ban the word &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prevent the application from returning to the same dangerous state that previously existed.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Turning Jinja Autoescape into a Regression Test
&lt;/h2&gt;

&lt;p&gt;The initial AI response display had previously used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| safe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I removed it and returned to Jinja's default autoescaping behavior.&lt;/p&gt;

&lt;p&gt;So I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_dashboard_initial_ai_binding_does_not_disable_autoescape
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This source guard checks that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ ai_advice }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is used and that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| safe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;has not returned to that rendering path.&lt;/p&gt;

&lt;p&gt;This makes it easier to detect a future regression where autoescaping is accidentally disabled again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Actually Rendering HTML-Like Text Through Jinja
&lt;/h2&gt;

&lt;p&gt;I added another test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_dashboard_initial_ai_advice_autoescapes_html_like_text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one goes further than inspecting the template source.&lt;/p&gt;

&lt;p&gt;For example, I pass strings such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;Test&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into the Jinja template.&lt;/p&gt;

&lt;p&gt;The test then confirms that they do &lt;strong&gt;not&lt;/strong&gt; become real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;elements.&lt;/p&gt;

&lt;p&gt;Instead, they must remain escaped text.&lt;/p&gt;

&lt;p&gt;This test actually renders the template and inspects the generated HTML using BeautifulSoup.&lt;/p&gt;

&lt;p&gt;So I now had both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A source guard&lt;/li&gt;
&lt;li&gt;A behavior-level rendering test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;protecting the same security boundary from different directions.&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest Results
&lt;/h2&gt;

&lt;p&gt;First, I checked test discovery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;--collect-only&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the time Phase 1 was completed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9 tests collected in 8.23s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All new tests were successfully discovered.&lt;/p&gt;

&lt;p&gt;Then I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9 passed in 6.37s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Passed: 9
Failed: 0
Skipped: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These nine tests represent the suite &lt;strong&gt;at the end of Phase 1&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I have continued adding regression tests since then as I discover more bugs and specifications worth protecting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1 Did Not Use PostgreSQL, Docker, or the Real Gemini API
&lt;/h2&gt;

&lt;p&gt;For this first phase, I intentionally focused on lightweight regression tests.&lt;/p&gt;

&lt;p&gt;The tests did &lt;strong&gt;not&lt;/strong&gt; require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;The real Gemini API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They also performed no real database operations.&lt;/p&gt;

&lt;p&gt;My priority was to build:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fast tests with minimal external dependencies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Database-related integration testing would come later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Files Changed
&lt;/h2&gt;

&lt;p&gt;Phase 1 changed four files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.github/workflows/test.yml
test_prompts.py
test_ai_integration.py
test_xss_regressions.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diff was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4 files changed, 195 insertions(+), 13 deletions(-)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did not modify the application code in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.py
prompts.py
models.py
templates/
migrations/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for this commit.&lt;/p&gt;

&lt;p&gt;The goal was not to change the existing implementation.&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lock the already-correct behavior in place with tests.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Commit
&lt;/h2&gt;

&lt;p&gt;After final verification, I created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;01d76e8 test: strengthen regression coverage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and pushed it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Number of Tests Matters Less Than What They Protect
&lt;/h3&gt;

&lt;p&gt;Previously, I had three pytest tests.&lt;/p&gt;

&lt;p&gt;But all three mostly examined one function.&lt;/p&gt;

&lt;p&gt;Numerically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 tests
        ↓
9 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;looks like the main improvement.&lt;/p&gt;

&lt;p&gt;It was not.&lt;/p&gt;

&lt;p&gt;The important improvement was that the protected surface expanded from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Basic prompt string checks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;XSS
Jinja autoescape
Prompt contracts
Gemini integration
Full CI test discovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test count is easy to measure.&lt;/p&gt;

&lt;p&gt;Protected behavior is what actually matters.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. “HTTP 200” Is Not Enough
&lt;/h3&gt;

&lt;p&gt;This work also reinforced another lesson:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;alone is a weak definition of success.&lt;/p&gt;

&lt;p&gt;A request can return successfully while:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The database ends up in the wrong state&lt;/li&gt;
&lt;li&gt;HTML is rendered unsafely&lt;/li&gt;
&lt;li&gt;Duplicate records are created&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So future tests need to check not only the response itself, but also:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What state exists after the request finishes?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  3. pytest Can Become an Incident-Prevention Ledger
&lt;/h3&gt;

&lt;p&gt;The biggest change was how I started thinking about pytest.&lt;/p&gt;

&lt;p&gt;Before this work, my mental model was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pytest
=
Check whether things work correctly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I think of it more like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A record of previous incidents, bugs, and near misses that automatically stops the application from returning to the same dangerous condition.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Discover XSS
        ↓
Fix it
        ↓
Add a regression test
        ↓
A future change recreates the unsafe state
        ↓
pytest fails
        ↓
CI turns red
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means I do not need to remember every security fix forever.&lt;/p&gt;

&lt;p&gt;The test suite remembers for me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Not “Never Make the Same Mistake Again,” but “Never Recreate the Same Dangerous State”
&lt;/h2&gt;

&lt;p&gt;This way of thinking comes partly from my day job.&lt;/p&gt;

&lt;p&gt;When an incident or mistake happens, saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I'll be more careful next time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not enough.&lt;/p&gt;

&lt;p&gt;That still relies on human attention.&lt;/p&gt;

&lt;p&gt;The stronger question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How can we prevent the same dangerous condition from existing again?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I found the same principle useful in software development.&lt;/p&gt;

&lt;p&gt;For XSS, the goal should not be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not enter dangerous HTML.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Even if HTML-like input arrives,
do not allow it to execute as HTML.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And pytest adds another layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If the safe implementation is broken later,
automatically stop the change.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That turns a lesson into a system.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Planned to Improve After Phase 1
&lt;/h2&gt;

&lt;p&gt;At the time this Phase 1 article was written, I had focused on lightweight regression tests that did not require external APIs or a database.&lt;/p&gt;

&lt;p&gt;Areas that were still largely unprotected included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL empty-database migrations&lt;/li&gt;
&lt;li&gt;Migration behavior on existing databases&lt;/li&gt;
&lt;li&gt;Authentication and authorization&lt;/li&gt;
&lt;li&gt;CSRF&lt;/li&gt;
&lt;li&gt;Invalid sales input&lt;/li&gt;
&lt;li&gt;Soft deletion and historical data preservation&lt;/li&gt;
&lt;li&gt;Duplicate daily-sales prevention&lt;/li&gt;
&lt;li&gt;Gemini API 429 responses&lt;/li&gt;
&lt;li&gt;Gemini API 503 responses&lt;/li&gt;
&lt;li&gt;General Gemini API exceptions&lt;/li&gt;
&lt;li&gt;API response behavior&lt;/li&gt;
&lt;li&gt;Dashboard aggregation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of changing all of these areas at once, I planned to expand coverage gradually using this cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Choose one dangerous state
        ↓
Write the test first
        ↓
Fix the implementation
        ↓
Run the full test suite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For database-related tests in particular, I also started paying more attention to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The state of the database after invalid input or a failed save&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;rather than checking only HTTP responses.&lt;/p&gt;

&lt;p&gt;Authentication, authorization, CSRF, and some other areas would be addressed in later phases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;During Phase 1, my pytest suite grew from 3 tests to 9.&lt;/p&gt;

&lt;p&gt;But increasing the number was not the real objective.&lt;/p&gt;

&lt;p&gt;The goal was to take bugs and vulnerabilities I had already discovered and preserve them as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Regression rules that prevent the same dangerous states from returning.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The workflow became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Discover
        ↓
Understand the cause
        ↓
Fix
        ↓
Add a regression test to pytest
        ↓
Verify automatically in CI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want pytest to evolve from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A tool that confirms normal behavior&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;into:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An incident-prevention ledger.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article records only the first phase.&lt;/p&gt;

&lt;p&gt;Since then, I have continued expanding the areas protected by tests using the same idea.&lt;/p&gt;

&lt;p&gt;The AI-response rendering implementation also evolved after publication.&lt;/p&gt;

&lt;p&gt;A reader suggested &lt;code&gt;innerText&lt;/code&gt;, so I tested it instead of accepting the suggestion immediately.&lt;/p&gt;

&lt;p&gt;That reinforced another principle I want to keep using:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Whether a suggestion comes from a human or an AI, verify it in your own environment before adopting it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;h2&gt;
  
  
  pytest Improvement Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/turning-pytest-into-an-incident-prevention-ledger-phase-1-from-3-simple-tests-to-9-2o53"&gt;Stage 1 — From 3 Simple Tests to 9&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-2-strengthening-sales-and-product-57b3"&gt;Stage 2 — Strengthening Sales and Product Registration&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-3-authentication-csrf-and-access-5gi2"&gt;Stage 3 — Authentication, CSRF, and Access Control&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-4-testing-failure-scenarios-71-87-12e7"&gt;Stage 4 — Testing Failure Scenarios&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tosane932/growing-pytest-into-an-incident-prevention-log-stage-5-questioning-green-with-mutation-1e8a"&gt;Stage 5 — Questioning GREEN with Mutation Testing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GitHub
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Qiita
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932" rel="noopener noreferrer"&gt;https://qiita.com/tosane932&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>pytest</category>
      <category>testing</category>
    </item>
    <item>
      <title>Securing AI Response Rendering in Flask — Replacing innerHTML After a Codex Review</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Fri, 14 Aug 2026 20:09:13 +0000</pubDate>
      <link>https://dev.to/tosane932/securing-ai-response-rendering-in-flask-replacing-innerhtml-after-a-codex-review-44m4</link>
      <guid>https://dev.to/tosane932/securing-ai-response-rendering-in-flask-replacing-innerhtml-after-a-codex-review-44m4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article mainly records the first fix I implemented, which used &lt;code&gt;createTextNode()&lt;/code&gt; together with &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; elements.&lt;/p&gt;

&lt;p&gt;After publication, a reader suggested using &lt;code&gt;innerText&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;I tested that approach in my own environment and confirmed that, for this particular use case, it preserved both line breaks and the important security property that HTML-like strings are not interpreted as HTML.&lt;/p&gt;

&lt;p&gt;The current implementation therefore uses &lt;code&gt;innerText&lt;/code&gt;, which is simpler.&lt;/p&gt;

&lt;p&gt;For that reason, the sections below describing &lt;code&gt;createTextNode()&lt;/code&gt;, &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; generation, and the resulting DOM structure should be understood as a record of the &lt;strong&gt;initial fix&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hello from Japan! 🇯🇵&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have been reviewing the security of a Flask application I am developing, with help from Codex.&lt;/p&gt;

&lt;p&gt;This time, I found a place where text returned by an AI model was being displayed in the browser using JavaScript &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The application appeared to work normally.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;innerHTML&lt;/code&gt; does not treat a string as plain text.&lt;/p&gt;

&lt;p&gt;It interprets that string as HTML.&lt;/p&gt;

&lt;p&gt;That means if an HTML-like string reaches the page through an AI response or another external input, the browser may create unintended elements.&lt;/p&gt;

&lt;p&gt;In the initial fix, I changed the rendering logic so that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;innerHTML&lt;/code&gt; was no longer used&lt;/li&gt;
&lt;li&gt;AI responses were treated as text nodes&lt;/li&gt;
&lt;li&gt;Only line breaks were created as &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; elements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I then verified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real Gemini responses&lt;/li&gt;
&lt;li&gt;DOM structure&lt;/li&gt;
&lt;li&gt;HTML-like test strings&lt;/li&gt;
&lt;li&gt;JavaScript errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After publishing the article, a comment led me to test an implementation using &lt;code&gt;innerText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That approach turned out to be simpler while preserving the behavior I needed, so the current implementation has since been refactored to use &lt;code&gt;innerText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Application
&lt;/h2&gt;

&lt;p&gt;The target is a sales management and analysis application I am developing for bakery stores.&lt;/p&gt;

&lt;p&gt;The main stack includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;Flask&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;SQLAlchemy&lt;/li&gt;
&lt;li&gt;Alembic&lt;/li&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;li&gt;Gemini API&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;pytest&lt;/li&gt;
&lt;li&gt;GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the application features sends sales data to the Gemini API and displays business-improvement advice returned by the model.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Codex Found
&lt;/h2&gt;

&lt;p&gt;During a Codex-assisted security review, I discovered that the AI-response rendering logic used &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, the dashboard contained code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;aiText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai_advice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI greeting on the input page used a similar pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;greetingText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original reason was simple:&lt;/p&gt;

&lt;p&gt;I wanted to convert line breaks into &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; elements.&lt;/p&gt;

&lt;p&gt;The problem was not the line breaks.&lt;/p&gt;

&lt;p&gt;The problem was &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, if this string were passed in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;b&amp;gt;AI Test&amp;lt;/b&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;innerHTML&lt;/code&gt; would not display &lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt; as ordinary characters.&lt;/p&gt;

&lt;p&gt;It would interpret the string as HTML markup.&lt;/p&gt;

&lt;p&gt;Because this feature displays AI-generated text, it is tempting to assume:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The AI will probably return only normal text.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And in normal operation, that may often appear to work.&lt;/p&gt;

&lt;p&gt;But I think a safer security principle is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not depend on dangerous input never arriving.&lt;br&gt;&lt;br&gt;
Design the rendering path so that even dangerous-looking input does not become dangerous.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Initial Jinja Rendering Also Used &lt;code&gt;safe&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The initial dashboard rendering also contained this Jinja expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ ai_advice | replace('\n', '&amp;lt;br&amp;gt;') | safe }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;safe&lt;/code&gt; disables Jinja's automatic escaping for that value.&lt;/p&gt;

&lt;p&gt;At the time, the initial message was fixed text, so this was not immediately exploitable in the same way.&lt;/p&gt;

&lt;p&gt;However, if that value later became dynamic, the same pattern could become dangerous.&lt;/p&gt;

&lt;p&gt;I therefore reviewed this path as part of the same fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Initial Fix
&lt;/h2&gt;

&lt;p&gt;This feature did not actually need to allow HTML.&lt;/p&gt;

&lt;p&gt;The only requirements were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Japanese text&lt;/li&gt;
&lt;li&gt;Multiple lines&lt;/li&gt;
&lt;li&gt;Bullet-like symbols&lt;/li&gt;
&lt;li&gt;Line breaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of adding a sanitization library such as DOMPurify, I chose a simpler rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not interpret the AI response as HTML at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The initial helper looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setTextWithLineBreaks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fragment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDocumentFragment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;br&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTextNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChildren&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTextNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this approach, even if the input contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;b&amp;gt;AI Test&amp;lt;/b&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the browser does not create a &lt;code&gt;b&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;It remains plain text.&lt;/p&gt;

&lt;p&gt;This was the implementation used in the first fix.&lt;/p&gt;

&lt;p&gt;After additional testing following publication, I later refactored the code to use &lt;code&gt;innerText&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Initial Fix Created Only Line Breaks as HTML Elements
&lt;/h2&gt;

&lt;p&gt;AI responses often contain multiple lines, so I wanted to preserve line breaks.&lt;/p&gt;

&lt;p&gt;In the first implementation, I split the response on &lt;code&gt;\n&lt;/code&gt; and inserted only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;br&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;between lines.&lt;/p&gt;

&lt;p&gt;The DOM therefore looked conceptually like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Text node
BR
Text node
BR
Text node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI response itself remained entirely inside text nodes.&lt;/p&gt;

&lt;p&gt;The only HTML elements created from this formatting process were the &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; elements.&lt;/p&gt;

&lt;p&gt;The current implementation achieves the same goal more simply by combining &lt;code&gt;innerText&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;white-space&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;pre-line&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Loading and Error Messages Were Changed to &lt;code&gt;textContent&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Fixed messages did not need &lt;code&gt;innerHTML&lt;/code&gt; either.&lt;/p&gt;

&lt;p&gt;I changed them to use &lt;code&gt;textContent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;aiText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌀 The AI assistant is thinking about detailed improvement ideas...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even for fixed text, using &lt;code&gt;textContent&lt;/code&gt; makes the intent clearer when HTML rendering is unnecessary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Removing Jinja &lt;code&gt;safe&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I also restored normal Jinja auto-escaping for the initial render.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ ai_advice }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line breaks were handled in CSS instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;white-space&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;pre-line&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preserved line breaks without generating HTML from the template.&lt;/p&gt;

&lt;p&gt;The same CSS remains useful in the current &lt;code&gt;innerText&lt;/code&gt; implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Confirming That Unnecessary &lt;code&gt;innerHTML&lt;/code&gt; Usage Was Gone
&lt;/h2&gt;

&lt;p&gt;Using Codex, I searched the target templates and confirmed that the unnecessary &lt;code&gt;innerHTML&lt;/code&gt; usage had been removed from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;templates/dashboard.html
templates/input.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those were the only two files changed in the initial fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  Browser Verification of the Initial Fix
&lt;/h2&gt;

&lt;p&gt;I did not stop after changing the code.&lt;/p&gt;

&lt;p&gt;For the initial implementation, I also used headless Chrome to verify the actual browser behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Dashboard Initial Render
&lt;/h3&gt;

&lt;p&gt;I checked the dashboard's initial state.&lt;/p&gt;

&lt;p&gt;The following remained normal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI business-advice area&lt;/li&gt;
&lt;li&gt;Buttons&lt;/li&gt;
&lt;li&gt;Charts&lt;/li&gt;
&lt;li&gt;Layout&lt;/li&gt;
&lt;li&gt;Horizontal overflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The original initial message did not contain a real line break, so I manually assigned this value in the browser DOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial line 1
Initial line 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verification result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lineBreakPreserved=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the line break was preserved.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Real Gemini Response
&lt;/h3&gt;

&lt;p&gt;Next, I made one real request to the Gemini API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP 200
Gemini AI advice generated successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI response displayed correctly in Japanese.&lt;/p&gt;

&lt;p&gt;Multiple lines were preserved.&lt;/p&gt;

&lt;p&gt;Markdown-like characters such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*
**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;were displayed as text rather than converted into HTML.&lt;/p&gt;

&lt;p&gt;For the initial implementation, the resulting DOM looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#text
BR
#text
BR
#text
BR
#text
BR
#text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The counts were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Text nodes: 5
BR elements: 4
Other child elements: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That confirmed that the AI response itself was not producing HTML elements.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. AI Greeting on the Input Page
&lt;/h3&gt;

&lt;p&gt;I also tested the AI greeting on the input page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP 200
AI daily greeting generated successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response displayed normally as two lines of Japanese text.&lt;/p&gt;

&lt;p&gt;The initial DOM structure was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#text
BR
#text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The counts were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Text nodes: 2
BR elements: 1
Other child elements: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The form and button layout also remained intact.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing with HTML-Like Strings
&lt;/h2&gt;

&lt;p&gt;Next, without sending anything to the database or Gemini API, I injected the following strings directly in the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;b&amp;gt;AI Test&amp;lt;/b&amp;gt;
&amp;lt;div&amp;gt;Test&amp;lt;/div&amp;gt;
&amp;lt;em&amp;gt;Safety Check&amp;lt;/em&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was the same on both the dashboard and input page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;b/div/em elements: 0
BR elements: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DOM was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#text
BR
#text
BR
#text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The text nodes literally contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;b&amp;gt;AI Test&amp;lt;/b&amp;gt;
&amp;lt;div&amp;gt;Test&amp;lt;/div&amp;gt;
&amp;lt;em&amp;gt;Safety Check&amp;lt;/em&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Represented as HTML, the browser state was equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;b&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;AI Test&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/b&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;
&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;Test&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/div&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;
&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;em&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;Safety Check&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/em&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;em&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;were not interpreted as real HTML elements.&lt;/p&gt;

&lt;p&gt;They remained ordinary text.&lt;/p&gt;

&lt;p&gt;Again, this DOM result describes the first implementation using &lt;code&gt;createTextNode()&lt;/code&gt; and &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checking for JavaScript Errors
&lt;/h2&gt;

&lt;p&gt;During the initial fix, I monitored Chrome DevTools Protocol events including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Runtime.exceptionThrown
console.error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JavaScript errors: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chrome itself produced some GPU and &lt;code&gt;inotify&lt;/code&gt; warnings, but those were unrelated to the application's JavaScript.&lt;/p&gt;




&lt;h2&gt;
  
  
  pytest
&lt;/h2&gt;

&lt;p&gt;I also ran the existing pytest suite at the time of writing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONDONTWRITEBYTECODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 pytest &lt;span class="nt"&gt;-p&lt;/span&gt; no:cacheprovider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;collected 3 items
test_prompts.py ... [100%]

3 passed in 0.40s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All existing tests passed.&lt;/p&gt;

&lt;p&gt;However, this revealed another problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;At the time, there were only three pytest tests, and none of them could automatically detect this XSS rendering issue.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That meant the bug could potentially return later without the existing tests noticing.&lt;/p&gt;

&lt;p&gt;This experience became one of the reasons I decided to strengthen pytest using actual bugs and vulnerabilities as regression tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checking the Git Diff
&lt;/h2&gt;

&lt;p&gt;Before committing the initial fix, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No whitespace errors were reported.&lt;/p&gt;

&lt;p&gt;The only changed files were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;M templates/dashboard.html
M templates/input.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Commit and Push
&lt;/h2&gt;

&lt;p&gt;After completing the initial verification, I created this commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f354d5d fix: sanitize AI response rendering
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I pushed it to &lt;code&gt;origin/main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After the article was published, a reader suggested testing &lt;code&gt;innerText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I did so and later refactored the implementation to the simpler version that is currently in use.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Do Not Design Around “The AI Probably Won't Return Anything Dangerous”
&lt;/h3&gt;

&lt;p&gt;Even if an AI normally returns ordinary prose, safety should not depend on that assumption.&lt;/p&gt;

&lt;p&gt;Whether the input comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AI model&lt;/li&gt;
&lt;li&gt;A user&lt;/li&gt;
&lt;li&gt;An external API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think the safer principle is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Treat external strings as strings.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2. If You Do Not Need HTML, Do Not Use &lt;code&gt;innerHTML&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;For this feature, I only needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Text&lt;/li&gt;
&lt;li&gt;Line breaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There was no real need for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initial fix used text nodes and &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; elements.&lt;/p&gt;

&lt;p&gt;After the article was published, I tested another approach and confirmed that, in this use case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;together with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;white-space&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;pre-line&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;could achieve the same goal with less code.&lt;/p&gt;

&lt;p&gt;The important lesson was not that one specific API is always correct.&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a value does not need to be interpreted as HTML, do not put it through an HTML-rendering API in the first place.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  3. Jinja &lt;code&gt;safe&lt;/code&gt; Is Useful, but It Disables Auto-Escaping
&lt;/h3&gt;

&lt;p&gt;Jinja's &lt;code&gt;safe&lt;/code&gt; filter is convenient.&lt;/p&gt;

&lt;p&gt;But if I cannot clearly explain why a value needs to bypass escaping, it is safer not to use it.&lt;/p&gt;

&lt;p&gt;In this case, the initial display could preserve line breaks using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;white-space&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;pre-line&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without disabling auto-escaping.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Inspecting the DOM Gives More Confidence Than Looking at the Screen Alone
&lt;/h3&gt;

&lt;p&gt;A page can look completely normal while still creating unintended HTML elements internally.&lt;/p&gt;

&lt;p&gt;During the first fix, checking the actual DOM structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Text node
BR
Text node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;made it much easier to verify that HTML-like strings were not being turned into elements.&lt;/p&gt;

&lt;p&gt;The implementation has since changed to &lt;code&gt;innerText&lt;/code&gt;, but the principle remains useful:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not verify only how the page looks. Verify how the browser is actually treating the data.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  5. Security Is About Preventing the Dangerous State, Not Just Avoiding the Same Mistake
&lt;/h3&gt;

&lt;p&gt;This was probably the strongest lesson for me.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Be careful not to enter dangerous HTML.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I prefer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build the system so that even if HTML-like text arrives, it is not executed as HTML.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This reminds me of safety management in my day job.&lt;/p&gt;

&lt;p&gt;When an incident or near miss happens, saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'll be more careful next time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is not enough.&lt;/p&gt;

&lt;p&gt;If the same conditions occur again, the same incident can happen again.&lt;/p&gt;

&lt;p&gt;A stronger approach is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Change the process so that the same dangerous state cannot be recreated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I found that the same idea applies to software security.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Want to Improve Next
&lt;/h2&gt;

&lt;p&gt;At the time this article was written, I had fixed the XSS risk in the AI response rendering and verified the behavior through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser testing&lt;/li&gt;
&lt;li&gt;DOM inspection&lt;/li&gt;
&lt;li&gt;pytest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, the pytest suite itself still had very narrow coverage.&lt;/p&gt;

&lt;p&gt;It could not automatically detect the vulnerability that had just been fixed.&lt;/p&gt;

&lt;p&gt;My next improvement is therefore to take real bugs and vulnerabilities that I have encountered and preserve them as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Regression tests that prevent the same dangerous state from returning.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of relying on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember not to make the same mistake again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I want the system to fail a test as soon as the same unsafe state is reintroduced.&lt;/p&gt;

&lt;p&gt;In the next article, I plan to strengthen pytest so that it becomes more than a general functional test suite.&lt;/p&gt;

&lt;p&gt;I want to treat it almost like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An incident-prevention ledger that records previous bugs, near misses, and vulnerabilities.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This issue was not an obvious failure that caused the application to stop working.&lt;/p&gt;

&lt;p&gt;The application looked normal.&lt;/p&gt;

&lt;p&gt;But a security weakness still existed underneath the visible behavior.&lt;/p&gt;

&lt;p&gt;In personal development, it is easy to focus mainly on adding features.&lt;/p&gt;

&lt;p&gt;However, I think systems become stronger when we repeatedly find and fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small bugs&lt;/li&gt;
&lt;li&gt;Small design weaknesses&lt;/li&gt;
&lt;li&gt;Small near misses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and preserve those lessons in tests.&lt;/p&gt;

&lt;p&gt;After this article was published, a reader suggested using &lt;code&gt;innerText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I did not adopt the suggestion immediately.&lt;/p&gt;

&lt;p&gt;Instead, I replaced the implementation in my own environment and verified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML-like strings were still not interpreted as HTML&lt;/li&gt;
&lt;li&gt;Line breaks remained visible&lt;/li&gt;
&lt;li&gt;Existing rendering behavior did not break&lt;/li&gt;
&lt;li&gt;Tests were unaffected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result was that &lt;code&gt;innerText&lt;/code&gt; could preserve the security properties and formatting I needed while making the implementation simpler.&lt;/p&gt;

&lt;p&gt;So the &lt;code&gt;createTextNode()&lt;/code&gt; and &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; implementation described earlier in this article is now a historical record of the &lt;strong&gt;first fix&lt;/strong&gt;, not the final implementation.&lt;/p&gt;

&lt;p&gt;This experience also reinforced another principle for me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Whether a suggestion comes from AI or from another developer, I should test it in my own environment before deciding to adopt it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I plan to continue using Codex not only for code generation, but as part of a wider cycle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Review → Fix → Verify → Prevent Regression&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tosane932/sales_data_app" rel="noopener noreferrer"&gt;https://github.com/tosane932/sales_data_app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Qiita:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/tosane932" rel="noopener noreferrer"&gt;https://qiita.com/tosane932&lt;/a&gt;&lt;/p&gt;

</description>
      <category>flask</category>
      <category>javascript</category>
      <category>security</category>
      <category>codex</category>
    </item>
    <item>
      <title>Existing DB Works, Empty DB Fails — Repairing a Broken Flask-Migrate History</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Sat, 08 Aug 2026 01:01:59 +0000</pubDate>
      <link>https://dev.to/tosane932/existing-db-works-empty-db-fails-repairing-a-broken-flask-migrate-history-220m</link>
      <guid>https://dev.to/tosane932/existing-db-works-empty-db-fails-repairing-a-broken-flask-migrate-history-220m</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hello from Japan! 🇯🇵&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article was originally published in Japanese on Qiita and has been translated and adapted for DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While reviewing the migration history of a sales management application built with Flask and PostgreSQL, I discovered a serious problem.&lt;/p&gt;

&lt;p&gt;The application worked normally with my existing local database.&lt;/p&gt;

&lt;p&gt;However, with a completely fresh PostgreSQL database, the current migration history could not build the required tables from scratch.&lt;/p&gt;

&lt;p&gt;The reason was simple but important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first migration did not create the tables. It started by adding a column to a table that was assumed to already exist.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Empty PostgreSQL database
        ↓
Add is_active column to products
        ↓
products does not exist
        ↓
Migration fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This article records the full process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How I discovered the problem&lt;/li&gt;
&lt;li&gt;How I investigated the migration history&lt;/li&gt;
&lt;li&gt;How I added a missing initial migration&lt;/li&gt;
&lt;li&gt;How I verified migration from an empty database&lt;/li&gt;
&lt;li&gt;How I verified that existing databases would not be damaged&lt;/li&gt;
&lt;li&gt;What I paid attention to while implementing and testing the fix&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Investigating and Verifying the Problem with Codex
&lt;/h2&gt;

&lt;p&gt;I used Codex in VS Code during this investigation and repair.&lt;/p&gt;

&lt;p&gt;However, I did not hand the entire task over to Codex.&lt;/p&gt;

&lt;p&gt;Instead, I divided the work into separate stages and restricted what Codex was allowed to do at each stage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Static investigation
        ↓
Design the repair
        ↓
Implement only the approved migration changes
        ↓
Static verification
        ↓
Dynamic test with an empty database
        ↓
Test against a clone of the existing database
        ↓
Final verification
        ↓
Commit and push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each stage, I explicitly defined conditions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not modify files during the investigation stage&lt;/li&gt;
&lt;li&gt;Limit implementation to exactly two migration files&lt;/li&gt;
&lt;li&gt;Do not directly migrate the normal local database&lt;/li&gt;
&lt;li&gt;Use a different Docker Compose project name for testing&lt;/li&gt;
&lt;li&gt;Test the existing database only through a clone created with &lt;code&gt;pg_dump&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Do not automatically fix additional problems that are discovered&lt;/li&gt;
&lt;li&gt;Allow commit and push only after final verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used Codex for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inspecting migration history&lt;/li&gt;
&lt;li&gt;Showing diffs&lt;/li&gt;
&lt;li&gt;Running verification commands&lt;/li&gt;
&lt;li&gt;Organizing the results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, I made the final decisions about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which repair strategy to use&lt;/li&gt;
&lt;li&gt;Safety conditions around databases&lt;/li&gt;
&lt;li&gt;Which resources could be deleted&lt;/li&gt;
&lt;li&gt;Whether the migration was safe enough to commit and push&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One lesson from this work was that AI coding assistance is not only about generating code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It is important to define the allowed change scope, prohibited operations, verification conditions, and stopping conditions.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Development Environment
&lt;/h2&gt;

&lt;p&gt;The main stack is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;Flask&lt;/li&gt;
&lt;li&gt;Flask-SQLAlchemy&lt;/li&gt;
&lt;li&gt;Flask-Migrate&lt;/li&gt;
&lt;li&gt;Alembic&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;Docker Compose&lt;/li&gt;
&lt;li&gt;pytest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the Docker container starts, migrations are applied before Gunicorn starts.&lt;/p&gt;

&lt;p&gt;Conceptually, the startup process looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;flask db upgrade &amp;amp;&amp;amp; gunicorn ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this setup, if the migration fails, Gunicorn is never started.&lt;/p&gt;

&lt;p&gt;That means the web application itself cannot start.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discovering the Problem
&lt;/h2&gt;

&lt;p&gt;When I checked &lt;code&gt;migrations/versions/&lt;/code&gt;, only one migration file existed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;043c481b4069_add_is_active_to_products.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file was the first revision in the migration history.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;revision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;043c481b4069&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;down_revision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, its &lt;code&gt;upgrade()&lt;/code&gt; function did not create the &lt;code&gt;products&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;It only added the &lt;code&gt;is_active&lt;/code&gt; column to an already existing &lt;code&gt;products&lt;/code&gt; table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upgrade&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch_alter_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;products&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;batch_op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;batch_op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;is_active&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;server_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;true&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words, there was no migration that created:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;products&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;daily_sales&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I searched the entire project and found no equivalent table-creation logic such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;products&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;daily_sales&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration history was incomplete.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Did the Existing Database Still Work?
&lt;/h2&gt;

&lt;p&gt;The existing database had most likely been created through another method before Flask-Migrate was introduced.&lt;/p&gt;

&lt;p&gt;The historical sequence was probably something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;products and daily_sales created outside Alembic
        ↓
Flask-Migrate introduced
        ↓
is_active added to products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the tables already existed, the existing database could apply the column-addition migration successfully.&lt;/p&gt;

&lt;p&gt;A fresh database was different.&lt;/p&gt;

&lt;p&gt;The first migration effectively tried to execute something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But an empty database had no &lt;code&gt;products&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;The failure therefore looked approximately like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;products does not exist
        ↓
First migration fails
        ↓
flask db upgrade exits with a non-zero status
        ↓
Gunicorn does not start
        ↓
The web application does not start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was the key issue:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The application worked because the existing database already contained history that Alembic itself could not reproduce.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Current Models
&lt;/h2&gt;

&lt;p&gt;The application currently contains two models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Product
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;server_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;true&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  DailySales
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DailySales&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;products.id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Repair Strategies I Considered
&lt;/h2&gt;

&lt;p&gt;I considered several approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add an Initial Migration Before the Existing Revision
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create initial tables
        ↓
Add is_active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preserves the existing history while adding the missing foundation.&lt;/p&gt;

&lt;p&gt;It creates a natural migration chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Rewrite the Existing First Migration
&lt;/h3&gt;

&lt;p&gt;Another option would be to change the existing revision so that it creates all current tables directly.&lt;/p&gt;

&lt;p&gt;However, that would significantly change the meaning of a migration that had already been applied.&lt;/p&gt;

&lt;p&gt;That would make the historical record less trustworthy.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Create a Conditional Migration
&lt;/h3&gt;

&lt;p&gt;Another possibility would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If products does not exist:
    create it
else:
    add the column
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This could support multiple database states.&lt;/p&gt;

&lt;p&gt;However, it would introduce more state-dependent behavior and make verification more complicated.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Use &lt;code&gt;db.create_all()&lt;/code&gt; and &lt;code&gt;stamp&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Another approach would be to create tables directly from the models and then use Alembic only to mark the database as being at a particular revision.&lt;/p&gt;

&lt;p&gt;This could solve the immediate problem quickly.&lt;/p&gt;

&lt;p&gt;However, it could create divergence between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The models&lt;/li&gt;
&lt;li&gt;The actual database&lt;/li&gt;
&lt;li&gt;The migration history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I therefore did not use this approach.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Approach I Chose
&lt;/h2&gt;

&lt;p&gt;I chose to insert an initial migration at the beginning of the history.&lt;/p&gt;

&lt;p&gt;The repaired migration chain became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;base
        ↓
b7e2c4a91f30 create products and daily_sales
        ↓
043c481b4069 add is_active to products
        ↓
head
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allowed the migration history to tell the actual story:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the initial tables&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;is_active&lt;/code&gt; later&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Implementing the Initial Migration
&lt;/h2&gt;

&lt;p&gt;I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;migrations/versions/b7e2c4a91f30_create_initial_tables.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;create initial products and daily_sales tables

Revision ID: b7e2c4a91f30
Revises:
Create Date: 2026-08-06
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;alembic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sa&lt;/span&gt;


&lt;span class="n"&gt;revision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;b7e2c4a91f30&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;down_revision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;branch_labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;depends_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upgrade&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;products&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;year&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;month&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PrimaryKeyConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;daily_sales&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;product_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ForeignKeyConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;product_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;products.id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PrimaryKeyConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;downgrade&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;daily_sales&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;products&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Table Creation Order Matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;daily_sales.product_id&lt;/code&gt; references &lt;code&gt;products.id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Therefore, the upgrade must create the tables in this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. products
2. daily_sales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The downgrade must remove them in reverse order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. daily_sales
2. products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;products&lt;/code&gt; were dropped first, the foreign key from &lt;code&gt;daily_sales&lt;/code&gt; could prevent the operation.&lt;/p&gt;

&lt;p&gt;This is a small detail, but an important one when creating migration history manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Did Not Put &lt;code&gt;is_active&lt;/code&gt; in the Initial Migration
&lt;/h2&gt;

&lt;p&gt;I deliberately did &lt;strong&gt;not&lt;/strong&gt; create &lt;code&gt;is_active&lt;/code&gt; in the initial migration.&lt;/p&gt;

&lt;p&gt;The history remained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base revision
└── Create products without is_active
        ↓
Existing revision
└── Add is_active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preserved the historical meaning of the existing revision.&lt;/p&gt;

&lt;p&gt;The initial migration alone does not need to match the current model.&lt;/p&gt;

&lt;p&gt;What matters is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Applying every revision through &lt;code&gt;head&lt;/code&gt; should produce the current model structure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was the goal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Changing the Existing Revision
&lt;/h2&gt;

&lt;p&gt;In the existing &lt;code&gt;043c481b4069&lt;/code&gt; migration, I changed only its revision relationship.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;revision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;043c481b4069&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;down_revision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;revision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;043c481b4069&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;down_revision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;b7e2c4a91f30&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also updated the &lt;code&gt;Revises&lt;/code&gt; field in its docstring.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-Revises:
&lt;/span&gt;&lt;span class="gi"&gt;+Revises: b7e2c4a91f30
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did &lt;strong&gt;not&lt;/strong&gt; change the actual &lt;code&gt;upgrade()&lt;/code&gt; or &lt;code&gt;downgrade()&lt;/code&gt; logic.&lt;/p&gt;

&lt;p&gt;The original operation remained intact.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Did Not Add a &lt;code&gt;server_default&lt;/code&gt; to &lt;code&gt;quantity&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;DailySales.quantity&lt;/code&gt; model has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a Python-side SQLAlchemy default.&lt;/p&gt;

&lt;p&gt;It is not a database-side default.&lt;/p&gt;

&lt;p&gt;A database default would instead be represented using something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;server_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I intentionally did not add that to the migration.&lt;/p&gt;

&lt;p&gt;Doing so would have introduced a database-level behavior that the current model did not define.&lt;/p&gt;

&lt;p&gt;I wanted the migration to reproduce the actual intended schema, not silently add extra behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  Static Verification Before Touching a Database
&lt;/h2&gt;

&lt;p&gt;Before starting PostgreSQL, I checked the syntax of both migration files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPYCACHEPREFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/sales_data_app_pycompile &lt;span class="se"&gt;\&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; py_compile &lt;span class="se"&gt;\&lt;/span&gt;
migrations/versions/b7e2c4a91f30_create_initial_tables.py &lt;span class="se"&gt;\&lt;/span&gt;
migrations/versions/043c481b4069_add_is_active_to_products.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was successful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exit code: 0
Syntax errors: none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also confirmed that the revision chain formed a single path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;base
        ↓
b7e2c4a91f30
        ↓
043c481b4069
        ↓
head
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after these static checks did I move on to database testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing a Completely Empty Database
&lt;/h2&gt;

&lt;p&gt;I did not want to affect my normal local database.&lt;/p&gt;

&lt;p&gt;Instead, I created an isolated environment by changing the Docker Compose project name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; sales_data_app_migration_test &lt;span class="se"&gt;\&lt;/span&gt;
  up &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a different Compose project name creates separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Containers&lt;/li&gt;
&lt;li&gt;Networks&lt;/li&gt;
&lt;li&gt;Volumes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal environment:
sales_data_app_postgres_data

Migration test environment:
sales_data_app_migration_test_postgres_data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test PostgreSQL database was completely empty.&lt;/p&gt;

&lt;p&gt;The startup logs showed the migrations running in the expected order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Running upgrade  -&amp;gt; b7e2c4a91f30,
create initial products and daily_sales tables

Running upgrade b7e2c4a91f30 -&amp;gt; 043c481b4069,
add is_active to products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gunicorn then started normally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starting gunicorn 26.0.0
Listening at: http://0.0.0.0:5000
Booting worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTTP request also succeeded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;200 text/html; charset=utf-8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirmed that the application could now start from a completely empty PostgreSQL database.&lt;/p&gt;




&lt;h2&gt;
  
  
  Schema Created from the Empty Database
&lt;/h2&gt;

&lt;p&gt;Three tables were created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alembic_version
products
daily_sales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;products&lt;/code&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;NULL&lt;/th&gt;
&lt;th&gt;DB Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;ID sequence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;year&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;month&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar(100)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;daily_sales&lt;/code&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;NULL&lt;/th&gt;
&lt;th&gt;DB Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;ID sequence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;product_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;date&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;quantity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The foreign key was also created as expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;daily_sales.product_id
        ↓
products.id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The delete and update behavior remained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ON DELETE: NO ACTION
ON UPDATE: NO ACTION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; behavior was added because it does not exist in the model.&lt;/p&gt;

&lt;p&gt;I also verified that the migration did not introduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extra UNIQUE constraints&lt;/li&gt;
&lt;li&gt;CHECK constraints&lt;/li&gt;
&lt;li&gt;Model-independent indexes&lt;/li&gt;
&lt;li&gt;A database-side default for &lt;code&gt;quantity&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final revision was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;043c481b4069
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Verifying an Existing Database
&lt;/h2&gt;

&lt;p&gt;The next question was more dangerous:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happens to a database that has already reached &lt;code&gt;043c481b4069&lt;/code&gt;?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I needed to confirm that the newly inserted ancestor migration would &lt;strong&gt;not&lt;/strong&gt; suddenly run against the existing database.&lt;/p&gt;

&lt;p&gt;I did not run this experiment directly against the real local database.&lt;/p&gt;

&lt;p&gt;Instead, I used the following process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start the normal database in read-only mode
        ↓
Create a pg_dump
        ↓
Stop the normal database
        ↓
Restore the dump into a separate PostgreSQL environment
        ↓
Run flask db upgrade only against the cloned database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The normal web container was not started during the dump process.&lt;/p&gt;

&lt;p&gt;I also used a read-only PostgreSQL setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PGOPTIONS=-c default_transaction_read_only=on
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Test the migration against data that behaves like the real database, without using the real database.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Running &lt;code&gt;upgrade&lt;/code&gt; Against the Cloned Database
&lt;/h2&gt;

&lt;p&gt;The cloned database initially contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alembic_version = 043c481b4069
products = 16 rows
daily_sales = 16 rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran the migration only against this cloned database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; sales_data_app_existing_migration_test &lt;span class="se"&gt;\&lt;/span&gt;
  run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--no-deps&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  web flask db upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command completed successfully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exit code: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Importantly, the logs did &lt;strong&gt;not&lt;/strong&gt; contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Running upgrade -&amp;gt; b7e2c4a91f30
create initial products and daily_sales tables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There were also no:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;operations and no duplicate-table errors.&lt;/p&gt;

&lt;p&gt;Alembic correctly treated the database as already being at &lt;code&gt;head&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The newly inserted ancestor revision was &lt;strong&gt;not&lt;/strong&gt; executed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparing the Database Before and After &lt;code&gt;upgrade&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I compared the cloned database before and after running &lt;code&gt;flask db upgrade&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The comparison included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;alembic_version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Table list&lt;/li&gt;
&lt;li&gt;Column names&lt;/li&gt;
&lt;li&gt;Data types&lt;/li&gt;
&lt;li&gt;NULL constraints&lt;/li&gt;
&lt;li&gt;Database defaults&lt;/li&gt;
&lt;li&gt;Primary keys&lt;/li&gt;
&lt;li&gt;Foreign keys&lt;/li&gt;
&lt;li&gt;UNIQUE constraints&lt;/li&gt;
&lt;li&gt;CHECK constraints&lt;/li&gt;
&lt;li&gt;Indexes&lt;/li&gt;
&lt;li&gt;Sequence states&lt;/li&gt;
&lt;li&gt;Every row in &lt;code&gt;products&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Every row in &lt;code&gt;daily_sales&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I exported all rows as CSV ordered by primary key and compared them using both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cmp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and SHA-256 hashes.&lt;/p&gt;

&lt;p&gt;Everything matched exactly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Revision&lt;/td&gt;
&lt;td&gt;&lt;code&gt;043c481b4069&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;043c481b4069&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;products&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16 rows&lt;/td&gt;
&lt;td&gt;16 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daily_sales&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16 rows&lt;/td&gt;
&lt;td&gt;16 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constraints&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Indexes&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequences&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All data&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This confirmed both migration paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Empty DB:
Base revision runs

Existing DB:
Base revision does not run again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was the result I needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running pytest
&lt;/h2&gt;

&lt;p&gt;Finally, I ran the existing test suite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONDONTWRITEBYTECODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
pytest &lt;span class="nt"&gt;-p&lt;/span&gt; no:cacheprovider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 passed in 0.06s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The existing tests also continued to pass after the migration repair.&lt;/p&gt;




&lt;h2&gt;
  
  
  Git Diff
&lt;/h2&gt;

&lt;p&gt;Only two migration-related files changed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;migrations/versions/
├── b7e2c4a91f30_create_initial_tables.py
└── 043c481b4069_add_is_active_to_products.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final commit was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9a4422e fix: add initial database migration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. “The Existing Database Works” Does Not Mean the Migration History Is Correct
&lt;/h3&gt;

&lt;p&gt;If an existing database already contains the necessary tables, an application can continue working even with incomplete migration history.&lt;/p&gt;

&lt;p&gt;But a new:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Development environment&lt;/li&gt;
&lt;li&gt;Test environment&lt;/li&gt;
&lt;li&gt;Machine&lt;/li&gt;
&lt;li&gt;Deployment target&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;may need to start from an empty database.&lt;/p&gt;

&lt;p&gt;That means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Existing environment starts successfully
≠
Migration history is correct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the biggest lesson from the incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Migrations Are About the Path, Not Only the Current Schema
&lt;/h3&gt;

&lt;p&gt;Even when:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current model
=
Current database schema
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that alone is not enough.&lt;/p&gt;

&lt;p&gt;The following path must also work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Empty database
        ↓
Apply every revision in order
        ↓
Reach the current schema
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A migration system is not only a description of the final schema.&lt;/p&gt;

&lt;p&gt;It is also the reproducible path used to reach that schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Modifying an Applied Revision Requires Careful Verification
&lt;/h3&gt;

&lt;p&gt;In this repair, I changed the &lt;code&gt;down_revision&lt;/code&gt; of an already applied migration.&lt;/p&gt;

&lt;p&gt;The migration graph was logically valid after the change.&lt;/p&gt;

&lt;p&gt;However, that was not enough evidence for me.&lt;/p&gt;

&lt;p&gt;I wanted to know how Alembic would treat a real existing database.&lt;/p&gt;

&lt;p&gt;That is why I tested it against a cloned database created from &lt;code&gt;pg_dump&lt;/code&gt;, instead of experimenting directly on the normal database.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test Both Empty and Existing Databases
&lt;/h3&gt;

&lt;p&gt;For migration repairs, I now think at least two paths should be tested:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Empty DB → upgrade head

2. Already migrated DB → upgrade head
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testing only one of these paths could leave either new environments or existing environments broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Even a Separate Compose Project Name Makes Testing Much Safer
&lt;/h3&gt;

&lt;p&gt;By changing the Docker Compose project name, I could create containers, networks, and volumes that were separate from my normal environment.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-p&lt;/span&gt; migration_test ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This made it much easier to test destructive or risky migration scenarios without touching the normal development environment.&lt;/p&gt;

&lt;p&gt;However, using a different project name alone is not enough.&lt;/p&gt;

&lt;p&gt;Before running anything, I still think it is important to verify the actual:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;volume names&lt;/li&gt;
&lt;li&gt;container names&lt;/li&gt;
&lt;li&gt;database connection targets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Isolation is useful, but only if I confirm that the environment is really the one I intended to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Tell AI When to Stop, Not Just What to Change
&lt;/h3&gt;

&lt;p&gt;When giving instructions to Codex, I explicitly included conditions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This stage is investigation only&lt;/li&gt;
&lt;li&gt;Do not start the database yet&lt;/li&gt;
&lt;li&gt;Do not automatically fix additional problems you discover&lt;/li&gt;
&lt;li&gt;Do not write to the normal database&lt;/li&gt;
&lt;li&gt;Before deleting anything, show me the target and wait for approval&lt;/li&gt;
&lt;li&gt;Commit and push only after final verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When using AI for development work, I found that specifying:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;what it should do&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;is only part of the instruction.&lt;/p&gt;

&lt;p&gt;It is also important to specify:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;where it must stop.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That makes unintended changes easier to prevent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The root problem was that the first migration did not create the initial tables.&lt;/p&gt;

&lt;p&gt;Instead, it assumed that &lt;code&gt;products&lt;/code&gt; already existed and immediately tried to add a column to it.&lt;/p&gt;

&lt;p&gt;Before the repair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Empty database
        ↓
Add is_active to products
        ↓
products does not exist
        ↓
Migration fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the repair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Empty database
        ↓
Create products
        ↓
Create daily_sales
        ↓
Add is_active to products
        ↓
Application starts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also verified the other direction using a cloned version of the existing database.&lt;/p&gt;

&lt;p&gt;The newly inserted base revision was not executed again, and the schema and data remained unchanged.&lt;/p&gt;

&lt;p&gt;The biggest lesson for me was that migrations should not only be checked by asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Does the application work with the database I already have?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I also need to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Can I start with an empty database and reach the current state using only the migration history?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was the missing piece in this migration history — and repairing it made the database setup reproducible again.&lt;/p&gt;

</description>
      <category>flask</category>
      <category>postgres</category>
      <category>alembic</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
