<?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>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>
    <item>
      <title>🔨 XSS Fixed in 47 Seconds? Using VS Code Codex as a Veteran Inspector from Another Department</title>
      <dc:creator>tosane932</dc:creator>
      <pubDate>Tue, 04 Aug 2026 02:25:56 +0000</pubDate>
      <link>https://dev.to/tosane932/xss-fixed-in-47-seconds-using-vs-code-codex-as-a-veteran-inspector-from-another-department-1dmb</link>
      <guid>https://dev.to/tosane932/xss-fixed-in-47-seconds-using-vs-code-codex-as-a-veteran-inspector-from-another-department-1dmb</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;My name is tosane932. I work as a professional truck driver in logistics while teaching myself Python and web application development.&lt;/p&gt;

&lt;p&gt;I began studying on May 12, 2026, and my total learning time has now reached &lt;strong&gt;152 hours&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This time, I used Codex in VS Code to perform a static review of a Flask application I am developing.&lt;/p&gt;

&lt;p&gt;The target was &lt;code&gt;sales_data_app&lt;/code&gt;, a sales management application for bakery stores.&lt;/p&gt;

&lt;p&gt;It currently supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product registration&lt;/li&gt;
&lt;li&gt;Daily sales quantity entry&lt;/li&gt;
&lt;li&gt;Sales rankings&lt;/li&gt;
&lt;li&gt;Chart.js visualizations&lt;/li&gt;
&lt;li&gt;Business advice through the Gemini API&lt;/li&gt;
&lt;li&gt;PostgreSQL data management&lt;/li&gt;
&lt;li&gt;Docker-based runtime environments&lt;/li&gt;
&lt;li&gt;Testing with pytest and GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had already reviewed the code myself several times.&lt;/p&gt;

&lt;p&gt;This time, however, I asked Codex to inspect the entire project and identify potential improvements.&lt;/p&gt;

&lt;p&gt;My conclusion was that Codex is extremely fast and highly capable at investigation.&lt;/p&gt;

&lt;p&gt;At the same time, asking it to fix every reported issue at once could damage existing features or the database.&lt;/p&gt;

&lt;p&gt;To me, Codex felt like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An experienced veteran employee transferred from another department who does not yet understand how this particular workplace operates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Its inspection ability is excellent.&lt;/p&gt;

&lt;p&gt;However, a human who understands the actual workplace still needs to decide what is a defect and what is intentional behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installing Codex in VS Code
&lt;/h2&gt;

&lt;p&gt;I installed the official Codex extension in VS Code.&lt;/p&gt;

&lt;p&gt;Codex can inspect files in the currently opened project and help with tasks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explaining code&lt;/li&gt;
&lt;li&gt;Investigating problems&lt;/li&gt;
&lt;li&gt;Editing files&lt;/li&gt;
&lt;li&gt;Running tests&lt;/li&gt;
&lt;li&gt;Executing terminal commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, my first instruction prevented the investigation from moving forward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please inspect the structure of this project.

Do not modify files or run terminal commands.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex responded that it had no way to inspect the file list or file contents without using terminal commands.&lt;/p&gt;

&lt;p&gt;I therefore revised the instruction to allow read-only commands while still prohibiting changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You may run read-only terminal commands required
to inspect the project structure.

However, the following actions are prohibited:

- Creating, modifying, or deleting files
- Installing packages
- Modifying the database
- Performing Git write operations
- Starting the application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this change, Codex could inspect the entire project in read-only mode.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rules I Set for Codex
&lt;/h2&gt;

&lt;p&gt;Giving Codex unrestricted access felt dangerous, so I added explicit working rules to the custom instructions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Working rules:

- Do not begin making changes without my explicit permission.
- First explain the facts you can confirm, possible causes,
  proposed fixes, affected areas, and required tests.
- Separate investigation from implementation.
  In principle, handle only one issue at a time.
- Do not mix unrelated changes into the same task.
- Do not run Git commit, push, reset, or rebase
  without explicit permission.
- Confirm with me before creating or deleting databases,
  running migrations, or modifying data.
- Confirm with me before deleting files,
  updating dependencies, or performing actions
  that may affect production.
- Read-only investigation commands are allowed.
- After making changes, report the modified files,
  the reason for each change, the affected areas,
  and the verification results.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also configured Codex to request approval before performing operations instead of giving it full access.&lt;/p&gt;

&lt;p&gt;Custom instructions describe expected behavior, but they are not a hard access-control system.&lt;/p&gt;

&lt;p&gt;For that reason, I used both configuration restrictions and written instructions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performing a Static Review of the Entire Project
&lt;/h2&gt;

&lt;p&gt;I asked Codex to review the whole project from several perspectives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inspect this entire project in read-only mode.

Report improvement candidates from the following perspectives:

- Obvious defects
- Security
- Maintainability
- Missing tests
- Unnecessary files or obsolete processing
- Differences between the README and implementation

Conditions:

- Do not create, modify, or delete files
- Do not modify the database
- Do not perform Git write operations
- Do not start the application or run tests yet
- Separate confirmed facts from assumptions
- Classify severity as high, medium, or low
- For each item, provide the relevant filename
  and location as evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex reported a total of &lt;strong&gt;18 improvement candidates&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The main findings were as follows.&lt;/p&gt;

&lt;h3&gt;
  
  
  High Severity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Migrations that might fail when building from an empty database&lt;/li&gt;
&lt;li&gt;Stored XSS in the dynamically rendered ranking section&lt;/li&gt;
&lt;li&gt;Public data modification without authentication&lt;/li&gt;
&lt;li&gt;Missing validation between sale dates, product dates, and sales status&lt;/li&gt;
&lt;li&gt;File permissions for &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Medium Severity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Repeated Gemini API execution without authentication&lt;/li&gt;
&lt;li&gt;Invalid input being treated as a server error or even a successful request&lt;/li&gt;
&lt;li&gt;No database-level prevention of duplicate sales for the same product and date&lt;/li&gt;
&lt;li&gt;Insufficient tests for important behavior&lt;/li&gt;
&lt;li&gt;Sales aggregation based only on product names&lt;/li&gt;
&lt;li&gt;Unpinned Chart.js version&lt;/li&gt;
&lt;li&gt;Containers running as the root user&lt;/li&gt;
&lt;li&gt;Development dependencies included in production&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Low Severity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Year selection fixed to 2026&lt;/li&gt;
&lt;li&gt;Images and videos no longer referenced from the README&lt;/li&gt;
&lt;li&gt;Local build artifacts and outdated documents&lt;/li&gt;
&lt;li&gt;Placeholder URLs remaining in JSON-LD&lt;/li&gt;
&lt;li&gt;Unclear relationship between CI and Render deployment conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codex organized the findings with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filenames&lt;/li&gt;
&lt;li&gt;Relevant lines&lt;/li&gt;
&lt;li&gt;Confirmed facts&lt;/li&gt;
&lt;li&gt;Possible effects&lt;/li&gt;
&lt;li&gt;Suggested improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The number of findings surprised me.&lt;/p&gt;

&lt;p&gt;However, asking Codex to fix all 18 issues at once would have been risky.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do Not Accept AI Findings Without Verification
&lt;/h2&gt;

&lt;p&gt;In the first review, Codex classified the &lt;code&gt;.env&lt;/code&gt; file permissions as high severity.&lt;/p&gt;

&lt;p&gt;I asked it to investigate the issue again.&lt;/p&gt;

&lt;p&gt;Codex then checked additional facts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/home/tosane&lt;/code&gt; had &lt;code&gt;0750&lt;/code&gt; permissions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; was excluded from Git&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; was not included in the Docker image&lt;/li&gt;
&lt;li&gt;Render did not directly use the local &lt;code&gt;.env&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;The additional risk was small in a single-user environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this investigation, Codex revised its own severity rating from &lt;strong&gt;high&lt;/strong&gt; to &lt;strong&gt;low or medium&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This demonstrated why the first AI response should not automatically be accepted.&lt;/p&gt;

&lt;p&gt;Even when an AI answer is detailed and confidently written, it may still be wrong or incomplete.&lt;/p&gt;

&lt;p&gt;I used the following process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Perform a static review of the entire project&lt;/li&gt;
&lt;li&gt;Recheck only the high-severity findings&lt;/li&gt;
&lt;li&gt;Separate reproducibility conditions from current practical impact&lt;/li&gt;
&lt;li&gt;Confirm the required specification before making changes&lt;/li&gt;
&lt;li&gt;Select only one issue as the first fix&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Choosing Stored XSS as the First Fix
&lt;/h2&gt;

&lt;p&gt;Of the 18 findings, I selected stored XSS in the dashboard’s dynamic ranking display as the first issue to fix.&lt;/p&gt;

&lt;p&gt;I chose it because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The change could be limited to one file&lt;/li&gt;
&lt;li&gt;It did not affect the database or authentication design&lt;/li&gt;
&lt;li&gt;Normal product-name display could be preserved&lt;/li&gt;
&lt;li&gt;The security benefit was clear&lt;/li&gt;
&lt;li&gt;Manual verification would be straightforward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problematic code inserted a product name retrieved from an API directly into an HTML string.&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod-name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated HTML string was later assigned to &lt;code&gt;innerHTML&lt;/code&gt;.&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;rankContainer&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;htmlContent&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;item[0]&lt;/code&gt; contained a product name entered by a user.&lt;/p&gt;

&lt;p&gt;If the name contained HTML or event attributes, the browser could interpret it as markup instead of plain text.&lt;/p&gt;

&lt;p&gt;Jinja2 auto-escaping protected the initial server-rendered page.&lt;/p&gt;

&lt;p&gt;However, after data was fetched through the Fetch API, the ranking was updated in JavaScript using &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That created a separate stored XSS risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Asking Codex for the Design Before Allowing Changes
&lt;/h2&gt;

&lt;p&gt;I did not immediately allow Codex to edit the file.&lt;/p&gt;

&lt;p&gt;First, I asked it to explain the proposed design.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Of the high-severity findings,
handle only the stored XSS issue
in the product-name ranking display.

Do not modify any files yet.

Explain the following:

1. The exact code location that requires modification
2. How to preserve the same display without using innerHTML
3. The files that would need to change
4. The impact on the existing ranking display
5. The expected behavior when product names contain
   Japanese text, symbols, or HTML-like strings
6. The tests required after the change

Do not handle the AI response display,
CSP, authentication, database,
or any other improvement in this task.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex proposed the following approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stop using &lt;code&gt;innerHTML&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create elements with &lt;code&gt;document.createElement()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Assign product names through &lt;code&gt;textContent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use a &lt;code&gt;DocumentFragment&lt;/code&gt; to append multiple rows efficiently&lt;/li&gt;
&lt;li&gt;Preserve the existing CSS classes and DOM structure&lt;/li&gt;
&lt;li&gt;Generate the empty-data message through the DOM API as well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It explained that the change could be completed entirely inside:&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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Allowing Codex to Implement the Fix
&lt;/h2&gt;

&lt;p&gt;After reviewing the scope, I gave Codex permission to implement only that fix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Implement only the stored XSS protection
for the dynamically generated product ranking.

Conditions:

- Modify only templates/dashboard.html
- Change only the ranking-update logic
- Do not touch the AI response, Chart.js,
  API, CSS, backend, or database
- Preserve existing class names and DOM structure
- Stop generating the ranking with innerHTML
- Use DOM APIs and textContent
- Do not run Git commit or push
- Do not run tests or start the application yet

After making the change, report:

1. The exact range changed
2. The difference before and after
3. Confirmation that no out-of-scope changes were made
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex completed the change in approximately &lt;strong&gt;47 seconds&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Updated Code
&lt;/h2&gt;

&lt;p&gt;The product name was assigned through &lt;code&gt;textContent&lt;/code&gt;.&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;const&lt;/span&gt; &lt;span class="nx"&gt;productName&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;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;div&lt;/span&gt;&lt;span class="dl"&gt;'&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;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prod-name&lt;/span&gt;&lt;span class="dl"&gt;'&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;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;&lt;code&gt;textContent&lt;/code&gt; does not interpret the value as HTML.&lt;/p&gt;

&lt;p&gt;For example, even if the following product name is stored:&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;Melon Bread&amp;lt;/b&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the browser displays it as text instead of creating a bold &lt;code&gt;b&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;Each ranking element is now created with the DOM API.&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;const&lt;/span&gt; &lt;span class="nx"&gt;rankingItem&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;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;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;rankingItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ranking-item&lt;/span&gt;&lt;span class="dl"&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;rankBadge&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;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;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;rankBadge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rank-badge&lt;/span&gt;&lt;span class="dl"&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;rank&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rankBadge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&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="s2"&gt;`rank-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;rankBadge&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quantity and unit are also created as separate elements instead of HTML strings.&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;const&lt;/span&gt; &lt;span class="nx"&gt;productQuantity&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;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;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;productQuantity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prod-qty&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;productQuantity&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&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;1&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;productUnit&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;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;span&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;productUnit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prod-unit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;productUnit&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;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;productQuantity&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="nx"&gt;productUnit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, the generated elements are appended together.&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;rankingItem&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="nx"&gt;rankBadge&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;productQuantity&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="nx"&gt;rankingItem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The previous ranking content is removed with:&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;rankContainer&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the existing CSS classes and DOM structure were preserved, the display logic became safer without changing the appearance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Manual Verification
&lt;/h2&gt;

&lt;p&gt;After the fix, I registered test products, entered sales quantities, and verified the results.&lt;/p&gt;

&lt;p&gt;The local environment and Render production environment use separate databases.&lt;/p&gt;

&lt;p&gt;For that reason, I registered the same test products again in Render and checked the behavior from my smartphone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Normal Product Name
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;It displayed normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Product Name Containing an HTML Tag
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;b&amp;gt;Melon Bread&amp;lt;/b&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was not displayed in bold.&lt;/p&gt;

&lt;p&gt;The browser treated it as plain text rather than HTML.&lt;/p&gt;

&lt;h3&gt;
  
  
  Product Name Containing Japanese Text and Symbols
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;あんぱん &amp;lt;限定&amp;gt; &amp;amp; コーヒー
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The characters &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;amp;&lt;/code&gt; displayed correctly.&lt;/p&gt;

&lt;p&gt;I found no problems with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product registration&lt;/li&gt;
&lt;li&gt;Sales quantity entry&lt;/li&gt;
&lt;li&gt;Dynamic ranking updates&lt;/li&gt;
&lt;li&gt;Dashboard charts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Final Check on the Render Demo from a Smartphone
&lt;/h3&gt;

&lt;p&gt;After deployment, I operated the public Render demo from my smartphone.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;b&amp;gt;メロンパン&amp;lt;/b&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;あんぱん &amp;lt;限定&amp;gt; &amp;amp; コーヒー&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I then entered daily sales quantities and confirmed that the products appeared correctly in both the ranking and chart.&lt;/p&gt;

&lt;p&gt;The HTML tags were not executed or rendered as formatting.&lt;/p&gt;

&lt;p&gt;The special characters &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;amp;&lt;/code&gt; were not lost.&lt;/p&gt;

&lt;p&gt;The existing sales-entry workflow, ranking, and charts also continued to work normally.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reviewing the Diff
&lt;/h2&gt;

&lt;p&gt;Codex reported that only &lt;code&gt;templates/dashboard.html&lt;/code&gt; had changed.&lt;/p&gt;

&lt;p&gt;I also reviewed the change manually with &lt;code&gt;git diff&lt;/code&gt;.&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;--&lt;/span&gt; templates/dashboard.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;git diff&lt;/code&gt; output was displayed through the &lt;code&gt;less&lt;/code&gt; pager.&lt;/p&gt;

&lt;p&gt;To exit the pager, press:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This does not close the terminal.&lt;/p&gt;

&lt;p&gt;It only closes the diff viewer and returns to the normal command prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Can Codex Be Used in Real Development?
&lt;/h2&gt;

&lt;p&gt;In this case, Codex did much more than generate code.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Inspection of the entire project structure&lt;/li&gt;
&lt;li&gt;Comparison between the README and implementation&lt;/li&gt;
&lt;li&gt;Identification of security risks&lt;/li&gt;
&lt;li&gt;Identification of the relevant files and lines&lt;/li&gt;
&lt;li&gt;Separation of confirmed facts from assumptions&lt;/li&gt;
&lt;li&gt;Clarification of reproduction conditions&lt;/li&gt;
&lt;li&gt;Analysis of the impact on existing features&lt;/li&gt;
&lt;li&gt;Suggestions for required tests&lt;/li&gt;
&lt;li&gt;A narrowly scoped implementation&lt;/li&gt;
&lt;li&gt;Reporting of the final changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ability to perform all of this quickly felt extremely powerful.&lt;/p&gt;

&lt;p&gt;A few years ago, a person would have needed to open files one by one, record the relevant line numbers, and prepare a report manually.&lt;/p&gt;

&lt;p&gt;Static-analysis tools and linters have existed for a long time.&lt;/p&gt;

&lt;p&gt;However, explaining a problem in Japanese while considering project context, likely impact, and possible fixes would still have required substantial human effort.&lt;/p&gt;

&lt;p&gt;That said, asking Codex to fix all 18 findings in one operation would have been dangerous.&lt;/p&gt;

&lt;p&gt;Some findings required business decisions before any code change.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Whether edits to historical sales should be allowed&lt;/li&gt;
&lt;li&gt;Whether discontinued products should allow corrections to past records&lt;/li&gt;
&lt;li&gt;Whether anyone should be able to operate the public demo&lt;/li&gt;
&lt;li&gt;Whether authentication should be mandatory&lt;/li&gt;
&lt;li&gt;How to repair migrations for an empty database&lt;/li&gt;
&lt;li&gt;How migration changes could affect the existing Render database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A technically reasonable change could still break the purpose of the application.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Veteran Inspector from Another Department
&lt;/h2&gt;

&lt;p&gt;The best logistics analogy I found was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Codex is an experienced veteran employee transferred from another department who does not yet understand this workplace.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It can quickly identify dangerous equipment, questionable procedures, and suspicious code.&lt;/p&gt;

&lt;p&gt;Its reports are fast and detailed.&lt;/p&gt;

&lt;p&gt;However, it does not automatically know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why the current workflow exists&lt;/li&gt;
&lt;li&gt;Which requirements have priority&lt;/li&gt;
&lt;li&gt;Which exceptions are necessary&lt;/li&gt;
&lt;li&gt;What the public demo is intended to allow&lt;/li&gt;
&lt;li&gt;Which existing behaviors must be preserved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A human reviewing the findings still needs to decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this truly a defect?&lt;/li&gt;
&lt;li&gt;Is it an intentional specification?&lt;/li&gt;
&lt;li&gt;Is it a future improvement?&lt;/li&gt;
&lt;li&gt;Does it need to be fixed immediately?&lt;/li&gt;
&lt;li&gt;What scope can be changed safely?&lt;/li&gt;
&lt;li&gt;Could the fix break another workflow?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before allowing Codex to modify the code, I followed this process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inspect the whole project in read-only mode&lt;/li&gt;
&lt;li&gt;Recheck the high-severity findings&lt;/li&gt;
&lt;li&gt;Correct the severity of an overestimated item&lt;/li&gt;
&lt;li&gt;Limit the first fix to one stored XSS issue&lt;/li&gt;
&lt;li&gt;Review the design and impact before implementation&lt;/li&gt;
&lt;li&gt;Limit the change to one file&lt;/li&gt;
&lt;li&gt;Allow Codex to implement the fix&lt;/li&gt;
&lt;li&gt;Review the diff with &lt;code&gt;git diff&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Register the test products locally and verify sales entry and dashboard display&lt;/li&gt;
&lt;li&gt;Commit the change and push it to GitHub&lt;/li&gt;
&lt;li&gt;Confirm completion of the Render deployment&lt;/li&gt;
&lt;li&gt;Register the same products again in Render because its database is separate&lt;/li&gt;
&lt;li&gt;Verify sales entry, rankings, and charts from a smartphone&lt;/li&gt;
&lt;li&gt;Confirm that both the local and production environments behaved normally&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Codex changed the code in approximately 47 seconds.&lt;/p&gt;

&lt;p&gt;However, making those 47 seconds safe required a human to read the findings, understand the specification, and define the allowed scope.&lt;/p&gt;




&lt;h2&gt;
  
  
  What AI Can Handle and What Humans Must Decide
&lt;/h2&gt;

&lt;p&gt;I organized the responsibilities as follows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tasks That Are Easy to Delegate to Codex
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Inspecting files in the project&lt;/li&gt;
&lt;li&gt;Identifying possible code issues&lt;/li&gt;
&lt;li&gt;Showing evidence and relevant locations&lt;/li&gt;
&lt;li&gt;Comparing possible fixes&lt;/li&gt;
&lt;li&gt;Making narrowly scoped code changes&lt;/li&gt;
&lt;li&gt;Reporting what changed&lt;/li&gt;
&lt;li&gt;Suggesting test cases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Decisions That Still Belong to Humans
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The purpose of the application&lt;/li&gt;
&lt;li&gt;Business rules&lt;/li&gt;
&lt;li&gt;Priorities&lt;/li&gt;
&lt;li&gt;Acceptable risk&lt;/li&gt;
&lt;li&gt;Public demo operation policies&lt;/li&gt;
&lt;li&gt;Database and production-change decisions&lt;/li&gt;
&lt;li&gt;Which findings to adopt or postpone&lt;/li&gt;
&lt;li&gt;Final verification and release decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codex works quickly when instructions are clear.&lt;/p&gt;

&lt;p&gt;However, giving it full access with vague instructions 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;Fix everything.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;could lead to a serious incident.&lt;/p&gt;

&lt;p&gt;The faster the worker, the more clearly the work area and stopping conditions must be defined.&lt;/p&gt;




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

&lt;p&gt;I used Codex in VS Code to perform a static review of a Flask application and fix one stored XSS issue.&lt;/p&gt;

&lt;p&gt;The final implementation changed only the dynamic ranking-generation logic in:&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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removing ranking generation through &lt;code&gt;innerHTML&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Creating DOM elements with &lt;code&gt;createElement()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Assigning product names through &lt;code&gt;textContent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Preserving the existing CSS classes and DOM structure&lt;/li&gt;
&lt;li&gt;Manually testing Japanese text, symbols, and HTML-like product names&lt;/li&gt;
&lt;li&gt;Confirming that normal product registration, sales entry, and ranking display still worked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codex demonstrated investigation and implementation speed that could be highly useful in real development.&lt;/p&gt;

&lt;p&gt;However, fixing every issue identified by AI is not automatically the correct decision.&lt;/p&gt;

&lt;p&gt;The important process is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Investigate&lt;br&gt;&lt;br&gt;
Recheck&lt;br&gt;&lt;br&gt;
Define the specification&lt;br&gt;&lt;br&gt;
Limit the change&lt;br&gt;&lt;br&gt;
Review the diff&lt;br&gt;&lt;br&gt;
Verify the actual behavior&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Codex is an extremely fast and capable worker.&lt;/p&gt;

&lt;p&gt;However, the decision about what to fix, what not to fix, and when the application is ready to ship still belongs to the human developer.&lt;/p&gt;

&lt;p&gt;My total learning time has now reached &lt;strong&gt;152 hours&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I will continue using AI without handing over the entire project blindly.&lt;/p&gt;

&lt;p&gt;My goal is to remain able to explain the cause, affected area, and reason for each change in my own words.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related Links
&lt;/h2&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;
  
  
  Live 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;

</description>
      <category>python</category>
      <category>flask</category>
      <category>security</category>
      <category>codex</category>
    </item>
  </channel>
</rss>
