<?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: Surat Mukker</title>
    <description>The latest articles on DEV Community by Surat Mukker (@smukker).</description>
    <link>https://dev.to/smukker</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%2F1908476%2Fbfe81852-cdf3-4ed4-8770-6656622f8162.jpg</url>
      <title>DEV Community: Surat Mukker</title>
      <link>https://dev.to/smukker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smukker"/>
    <language>en</language>
    <item>
      <title>Lesson 3 - Architecture: Learn to organize your thoughts</title>
      <dc:creator>Surat Mukker</dc:creator>
      <pubDate>Thu, 30 Jul 2026 00:33:26 +0000</pubDate>
      <link>https://dev.to/smukker/lesson-3-architecture-learn-to-organize-your-thoughts-2h</link>
      <guid>https://dev.to/smukker/lesson-3-architecture-learn-to-organize-your-thoughts-2h</guid>
      <description>&lt;h2&gt;
  
  
  AI takes the path of least resistance.
&lt;/h2&gt;

&lt;p&gt;That one characteristic explains most of what changed for me about architecting a system once an agent was in the loop.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;genuinely faster than I am on frameworks, patterns, and the standard way to wire something up&lt;/strong&gt;. Since it has read more on them than I have. But "least resistance" means it optimizes for the thing in front of it, e.g., getting an endpoint to work or a test to pass. It cannot optimize for the shape the system needs for your use case because it does not know all details. You still own 100% of that part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways least-resistance goes wrong
&lt;/h2&gt;

&lt;p&gt;Left alone, the path of least resistance breaks in two opposite directions. It cuts a corner to make the immediate thing work: collapses a boundary, hardcodes a value, skips the seam that would have let two pieces move independently later. And when you try to correct it, it will over-engineer and reach for patterns, layers, and abstractions you did not ask for and don't need yet.&lt;/p&gt;

&lt;p&gt;Both come from the same place: &lt;strong&gt;it is solving the prompt, not steering the architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Here is the version I lived with. My app is layered the usual way: an API layer, a service layer under it, a data-access layer under that, with clear rules about what each one is allowed to do. Database transactions belong in the service layer. The agent kept ignoring that. Commits I had scoped to the service layer kept turning up in the data layer, or up in the API. The worst one was a transaction that opened in the service layer and got committed two layers down. If left at simple prompts, it will run and deliver you something that works, but you'll find that along the way it has quietly broken the boundary and created a brittle system.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Here is the flip side, from just the other day. I was working on a bug fix with the agent on its own branch off main. Mid-test, I hit a separate gap, related to the feature but not the bug, and asked the agent to fix that too. It sensibly put the gap on its own branch, but branched off main instead of off the bug-fix branch, and I missed that. Both were named &lt;code&gt;fix/...&lt;/code&gt;, so at a glance nothing looked off. It made the change and, per my workflow, asked me to test before it committed. The gap was handled well, but the bug was back. When I flagged it, the agent agreed: of course, that's expected since this branch came off main.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;So I asked it to reconcile the two. The plan it came back with made my head spin: commit the work on the stray branch, rebase that onto the bug-fix branch, fast-forward the target, delete the stray branch, then re-verify the whole thing. But nothing was committed yet. The entire fix was three commands: &lt;code&gt;git stash&lt;/code&gt;, switch to the bug-fix branch, &lt;code&gt;git stash pop&lt;/code&gt;, then commit on the right branch. When I asked why not just do that, it agreed at once: "you're right, that was over-engineered."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A cut corner does not stay cut
&lt;/h2&gt;

&lt;p&gt;This is the part that surprised me. Even when there was a good reason at the time, a cut corner does not stay a local mistake. Future sessions keep finding it and repeating it. The agent reads the current state of the code, sees the shortcut, and treats it as the pattern to follow. Murphy's Law seems to be in full play here. You'll have 10 good examples it could have followed, but it will find the one that it should not.&lt;/p&gt;

&lt;p&gt;That boundary problem was exactly this. It was not a one-time slip. The agent generated code that broke the layering over and over, every session, and every time I had to catch it and send it back. Early on I would just fix it by hand. That was the mistake. &lt;strong&gt;Fixing it myself did nothing to stop the next session from doing it again.&lt;/strong&gt; So I stopped hand-fixing and started making the agent fix it and write the rule down, where the next session would read it and follow it. But it still is not 100% foolproof.&lt;/p&gt;

&lt;p&gt;Two things turned it around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan reviews and a convention check became a fixed part of my workflow.&lt;/strong&gt; Until they were, a lot of my attention was going to catching these leaks instead of the actual work. Once they were in the loop the leaks mostly stopped, and I ran a dedicated cleanup milestone to fix the ones that had already slipped through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation.&lt;/strong&gt; I keep a full separate system-test stack and a dev stack for manual testing, non-negotiable. For anything parallel, I use worktrees, so each line of work has its own isolated stack and nothing bleeds across. Exploration you leave sitting in the main context is exploration the agent keeps copying, so keep the main context clean.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rhythm I settled into on new work: experiment, clean up, then move on. &lt;strong&gt;The cleanup is not optional&lt;/strong&gt;. It's what stops today's throwaway work/context from unintentionally becoming part of your conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  You bring the vision; it fills in the frameworks
&lt;/h2&gt;

&lt;p&gt;The division of labor that works for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You own the judgment calls and the direction.&lt;/strong&gt; State the bigger vision up front, even if you don't have every detail yet. AI can help you work out the details, but only against a destination you have set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It knows the frameworks; it does not know your context&lt;/strong&gt;. It's more up to speed than we are on approaches and trade-offs in the abstract. It does not know your constraints, your users, your history, any of it, until you tell it.&lt;/li&gt;
&lt;li&gt;When it hands you a choice you don't understand, &lt;strong&gt;make it explain&lt;/strong&gt;. Don't rubber-stamp an approach because it sounds authoritative. Ask it to walk you through the options and the trade-offs, then you decide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Example: When it was time to integrate LLMs into the product I was building, I decided to make it a plugin-based system that could be driven via configs and specs. I spent a good amount of time going back and forth with it on requirements and goals. After many iterations, I had a design for an aspirational system that was the shape I wanted. Then we went back and forth on pieces that we would build now and others that would come later as needed. And then I asked it to add a lever: because it all deploys to a single container anyway, it can run the code built in instead of loading plugins every time. The agent was fine building the plugin machinery, but it would not have warned me that the pure-plugin path is a very painful operational choice.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI can build it. You still need to define precisely for it and own the decisions.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Destination and constraints first, then a phased plan
&lt;/h2&gt;

&lt;p&gt;The biggest shift is where I start. Not "let me build this piece and figure the rest out later." That's how you get architected into a corner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with where you want to go and what bounds you&lt;/strong&gt;: resources, timing, process. Then work out a phased plan to get there, and write it to disk as it evolves. Don't wait for the plan to be final before it exists on paper. A plan being figured out in the open is worth more than a perfect one still in your head, and the document keeps the history of the decisions. When you need to change something later, it tells you why it was done that way, which is exactly what you likely have forgotten by then.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A phased plan that paid off: Coming back to the same example, the AI service started as a library, imported straight into the app. Then I needed a second app that wanted almost all the same prompts. The quick path was to import it as a library again. Instead, I set the goal for the service to stand on its own over HTTP so any app can share it, and phased the way there. Stand it up as a microservice. Build the new app against it over HTTP. Keep the original app on the library path behind a flag. Prove the HTTP path works with the new app first, then migrate the original app over. The flag meant nothing broke while the new shape was still being proven.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I keep
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Architecting with AI did not lower the bar on judgment. It raised it&lt;/strong&gt;. And it's very empowering if used correctly. For me, the agent removed the friction that used to slow me down, but it cuts both ways: because it does not have context (either you didn't specify or it rolled out of the current window), it's just as likely to speed you toward the wrong structure as the right one. &lt;strong&gt;The direction has to come from you, stated early, written down, and enforced.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I am still refining how much of the planning to hand over and how much to keep in my own head. But organizing your own thinking pays off in spades: get that right, and you can get the agent to follow as well.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;A note on tools: I keep this deliberately tool-agnostic. Where it matters, I mean "the agent" generically. The models and tools are moving fast, so anything specific here is a snapshot, not a verdict.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>ai</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Lesson 2 - Security: Secure as you build</title>
      <dc:creator>Surat Mukker</dc:creator>
      <pubDate>Tue, 14 Jul 2026 19:09:11 +0000</pubDate>
      <link>https://dev.to/smukker/lesson-2-security-secure-as-you-build-3c5b</link>
      <guid>https://dev.to/smukker/lesson-2-security-secure-as-you-build-3c5b</guid>
      <description>&lt;p&gt;&lt;strong&gt;Filing security under later does not work&lt;/strong&gt;. For most of my career, when a vulnerability got disclosed, attackers needed days, often weeks, to turn it into a weapon. Defenders had a head start, and it's not the case anymore.&lt;/p&gt;

&lt;p&gt;A few days from calling the build ready, one of the packages I lean on got poisoned. A release of a core dependency, one that sat deep in my stack, went up on the package index carrying a deliberate compromise. Not a bug. A rigged version built to harvest secrets: the keys, tokens, and passwords a developer has lying around. It got caught and pulled fast, but that doesn't help when it's already sitting in your own dependency tree.&lt;/p&gt;

&lt;p&gt;It hit hard because I already knew AI was making the challenge of securing what we build worse. I'd read about it and nodded along, then deferred the library hardening until the app was built and production-ready. The warning had been in front of me for months, filed under later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exposures
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;And it wasn't a one-off. That same week, a second dependency I relied on was hijacked in a similar way, a remote-access trojan slipped into its latest version. Two compromises, two packages I used, within seven days.&lt;/li&gt;
&lt;li&gt;That is the pace now. Poisoned code goes live and lands in thousands of dependency trees before the advisory is even written. The same speed that lets us build lets them attack.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Security First
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I threw out the ship-it-then-secure-it plan. I stopped what I was doing and dealt with security first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;In the AI era, security is not a phase after the build but a part of it.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Assessment
&lt;/h3&gt;

&lt;p&gt;Before I changed a thing, I worked out whether it had actually reached me.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was it ever pulled into a production build? No. I hadn't built for prod during the exposure window, so the rigged version never got there.&lt;/li&gt;
&lt;li&gt;Locally? I'd been in active development with hot-reload on, so I never triggered the full rebuild that would have freshly installed it. Most likely clear, not certain.&lt;/li&gt;
&lt;li&gt;What could it have reached? This package sat deep in my stack, close to my environment and my secrets, and the compromised code was built to hunt for exactly that. So I stopped guessing and treated every key and token as already compromised.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mitigation
&lt;/h3&gt;

&lt;p&gt;Once I knew roughly where I stood, I hardened past the incident, not just against it. &lt;strong&gt;Every dependency you add is someone else's attack surface borrowed into yours.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rotated every token.&lt;/li&gt;
&lt;li&gt;Properly segregated staging and production. They'd been sharing more than they should, a cache instance and some credentials I'd waved off as low-risk.&lt;/li&gt;
&lt;li&gt;Pinned every dependency, frontend and backend, to exact versions by hash. Nothing floating, no silent updates.&lt;/li&gt;
&lt;li&gt;Stood up a dependency-review framework so new bumps get looked at instead of auto-trusted.&lt;/li&gt;
&lt;li&gt;Changed when I reach for a library at all. When the second one got hit, I didn't wait for a clean version. It was small enough that I pulled it out and wrote the few lines I actually needed myself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gap
&lt;/h3&gt;

&lt;p&gt;The dependency scare wasn't the only place I'd gone loose. Around the same time, I ran a proper security review and found something closer to home.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Once I'd stopped reading every line, the agent had quietly dropped an authorization check. The scoping that should have kept one tenant's data walled off from another was just gone.&lt;/li&gt;
&lt;li&gt;It hadn't caused a breach; the identifiers weren't guessable, but the gate was standing open.&lt;/li&gt;
&lt;li&gt;That is now a standing rule: a security review runs as a gate inside the build loop, not a pass tacked on at the end. When AI is writing this much of the code, we pull every review left, security included, or the code rots as fast as we build it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Manual Levers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Not all of it is automated, and that's deliberate. Service tokens rotate on their own now. Password rotation is streamlined, but I still choose to run it myself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Lane
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The most important lane in software right now is AI-native security: prevention, threat assessment, and mitigation built for a world where attackers move at machine speed.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;You don't bolt this on at the end. It decides whether everything else is even safe to ship.&lt;/li&gt;
&lt;li&gt;That week took away the last illusion that there was time to prepare. Getting the edge back means defending as fast as they now attack.&lt;/li&gt;
&lt;li&gt;It's a good time to be in this lane. If you're building here, or thinking seriously about it, I'd love to compare notes.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>supplychain</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Lesson 0 - Learning to build with AI: Figuring Out Trust boundaries</title>
      <dc:creator>Surat Mukker</dc:creator>
      <pubDate>Tue, 14 Jul 2026 03:08:09 +0000</pubDate>
      <link>https://dev.to/smukker/lesson-0-learning-to-build-with-ai-where-i-learned-not-to-trust-it-49hf</link>
      <guid>https://dev.to/smukker/lesson-0-learning-to-build-with-ai-where-i-learned-not-to-trust-it-49hf</guid>
      <description>&lt;p&gt;After publishing the first lesson, I realized I should have started the series with why I started to build with AI, what I learned, and how it shaped my view of software development in the age of AI.&lt;/p&gt;

&lt;p&gt;I've led engineering teams for about 25 years. Lately I've been back in the code myself. Over the last several months I built a product end to end, mostly on my own: an AI-native procurement tool, first plan through deployment, with AI in the loop the whole way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero to production in about four months.&lt;/li&gt;
&lt;li&gt;A few thousand tests behind it.&lt;/li&gt;
&lt;li&gt;Multiple LLM providers, picked by the use case.&lt;/li&gt;
&lt;li&gt;The prompt drives the choice of vendor and model based on what it needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to understand these tools by using them, not by reading about them. What follows is what I learned, the parts that held up and the parts that didn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Plan
&lt;/h3&gt;

&lt;p&gt;The plan was to build it mostly myself, use AI to move faster, and own every design decision. That last part never changed. AI helps me weigh options and think through how a choice plays out before I commit, but the call stays mine. After enough years leading teams, I don't like shipping a system I can't fully account for.&lt;/p&gt;

&lt;p&gt;That ran straight into my own gap. I've built plenty in Java, some from scratch, some I picked up and extended. Python I'm comfortable in, but I'd never built a full app in it solo, start to finish. So I was leaning on AI hardest in exactly the place I was thinnest. That's what made the choice of tool matter more than I expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Tools
&lt;/h3&gt;

&lt;p&gt;I started on Gemini. Good at plenty, but it fell apart on the one thing that mattered right then: it could not get its own unit tests to pass. It made a mess of mocks and kept digging itself deeper into a hole instead of fixing it. It couldn't tell me the what or why of the failures.&lt;/p&gt;

&lt;p&gt;I decided to try Claude, and asked it to help me understand the reasons for the failures and fix them. Within 30 minutes everything was passing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One caveat, and it holds for the whole post: none of this is an endorsement. I'm not putting one tool above another. I happened to build on a couple of specific ones, and both have moved on a lot since late 2025, so that afternoon was a snapshot, not a verdict. What follows isn't about which agent to pick. It's about how to work with any of them, because the hard parts I hit are the same whatever is doing the typing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In fact, this is exactly why I built multi-model validation into the architecture from the ground up, instead of trusting any one model's first answer. Different models catch different things. And even a single model gives you more than its first pass if you ask for it. Hand it a leading question, tell it to review an answer, including its own, and it turns honest and digs deeper instead of defending what it just said. The catch is on us. We have to learn to ask, and remember to.&lt;/p&gt;

&lt;p&gt;Tests stayed my Achilles heel for a long time. Every time they failed I'd point the agent at them and it would spin and get lost. So I tried giving it the failures in batches, and it did much better. The real turning point, where the tests started to pay off, came after a complete refactor of the test system (see &lt;a href="https://dev.to/smukker/lesson-1-tdd-with-ai-getting-tests-that-hold-up-when-the-agent-writes-them-1he"&gt;Lesson 1&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The bigger thing I took from all this: the value isn't autocomplete. Used well, it's a tool that levels you up in the areas you're thin on, and I had one of those open in front of me. I stopped treating it like a faster keyboard and started treating it like a fast, literal engineer I had to manage.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Setbacks
&lt;/h3&gt;

&lt;p&gt;The prototypes came fast, faster than I could keep up with, if keeping up means actually understanding what's running in my own system. Nobody warns you about that part. &lt;strong&gt;Getting AI to produce code is the easy bit. Staying on top of what it produces is the real work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the version that made it concrete. I wrote a plan, the agent executed it, and everything looked done. Days later, working on something else, I found it had quietly decided a few things weren't important and dropped them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audit date fields, cut because it judged them unnecessary.&lt;/li&gt;
&lt;li&gt;A service boundary, collapsed because, in its words, it's all one container right now anyway.&lt;/li&gt;
&lt;li&gt;The high-value tests, skipped while the easy ones got written, and it never said so out loud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this was malicious. It had a reasonable-sounding rationale for every cut, and that is exactly what makes it dangerous. The corners it chooses to cut are the ones it can argue for, so scanning the diff and asking "does this look right" won't catch them. It always looks right.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fixes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Got the plan out of the chat and onto disk.&lt;/strong&gt; Plan files, tracker tables. Intent and decisions live in files now, not in a conversation that vanishes the moment a test run derails it. That's not hypothetical: it once lost an entire plan mid-session because the plan only ever existed in the chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turned my review instincts into gates.&lt;/strong&gt; A loop the work has to pass through: plan, review the plan, implement, then conventions, security, and test gates before anything ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed AI as a Contributor.&lt;/strong&gt; I didn't sit down and design that to mirror how I ran human teams. It just emerged. Then I noticed it was the same review culture I'd always run, rebuilt for a contributor that's faster and far more agreeable than any junior I've worked with.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One standing instruction mattered more than all the tooling: &lt;strong&gt;challenge me&lt;/strong&gt;. Don't just do what I ask. If what I'm asking will age badly, say so before you build it. An agent won't push back unless you tell it to. A good engineer does it without being asked.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Unlock
&lt;/h3&gt;

&lt;p&gt;For a while I didn't even run the insights report. I figured it was a distraction I didn't want to deal with. One day I ran it anyway, and it changed how I worked. It was blunt: what was working, what was holding me back, concrete suggestions on both. Then the tool offered to help me act on the feedback. I said yes.&lt;/p&gt;

&lt;p&gt;The best call I made was to spin those improvements out into their own separate project. Everyone had been talking about skills and commands and agents and posting their own, and honestly, it was a bit intimidating while I was still struggling just to keep the agent on the rails. That toolkit project is what opened the door to working with AI effectively and efficiently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So my advice to you is just go ahead and start. Use AI in your real workflow and it will help you figure it out as you go. You don't need to train up or set aside time to learn it first. If you write code, open an AI-enabled IDE and start coding with AI. The rest falls into place on its own.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Now
&lt;/h3&gt;

&lt;p&gt;Today I barely type. I give requirements, clarify, iterate on a plan, and the agent does the build.&lt;/p&gt;

&lt;p&gt;The honest part: it is not faster than working without the system I built, but it is a lot more efficient. The gates and the checks cost real time. What I get back is output that's actually to spec, and a system I still understand well enough to debug at 2 am. I traded speed for fidelity, on purpose. At a certain level of seniority, that's the trade you want.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Future
&lt;/h3&gt;

&lt;p&gt;I haven't pushed it to run on a cloud VM yet. It needs more harness around it, and when I get there, I want it multi-model and fully pipelined end to end. I have outlines of a framework, but I haven't given it the time it deserves yet. If anyone is interested in this or wants to collaborate, please reach out.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lesson
&lt;/h3&gt;

&lt;p&gt;None of this replaced the engineering. It just moved. Less of my day is typing now. Most of it is deciding what good actually looks like and building the guardrails that hold when a fast, agreeable, slightly overconfident contributor is the one doing the typing.&lt;/p&gt;

&lt;p&gt;I don't think the people who get the most out of AI are the ones who trust it the most. It's the ones who work out the few places it can't be left alone, and stay there. For me that took months of getting it wrong first, which is most of what this post was about.&lt;/p&gt;

&lt;p&gt;The product was the reason I started. The method is what I'm keeping.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>engineering</category>
      <category>leadership</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Lesson 1 - TDD with AI: getting tests that hold up when the agent writes them</title>
      <dc:creator>Surat Mukker</dc:creator>
      <pubDate>Fri, 10 Jul 2026 19:43:42 +0000</pubDate>
      <link>https://dev.to/smukker/lesson-1-tdd-with-ai-getting-tests-that-hold-up-when-the-agent-writes-them-1he</link>
      <guid>https://dev.to/smukker/lesson-1-tdd-with-ai-getting-tests-that-hold-up-when-the-agent-writes-them-1he</guid>
      <description>&lt;p&gt;I let the agent write the tests as it built the features. They passed. I ran them, tested by hand, everything green, let it commit. For a while that felt great.&lt;/p&gt;

&lt;p&gt;Then I changed a model. A field moved, a type changed, and a pile of tests went red. Not the tests for the thing I changed, tests all over the suite. I fixed them. A week later I changed another model, and it happened again. This kept happening until I stopped and looked at why.&lt;/p&gt;

&lt;p&gt;Here's what made it sting. I've been writing tests for a long time, back to my Java days, and the thing I believe about them is simple. A unit test doesn't pay you back the day you write it. It pays you back later, the day you change the code and it catches what you broke. You spend a little now to buy yourself safety in the future. That's the whole deal.&lt;/p&gt;

&lt;p&gt;These tests ran that backwards. They cost almost nothing to write and a fortune to keep. Every change broke them, and not because they'd caught a real problem. The one thing tests are supposed to make safe, changing the code, was the exact thing they made painful. That's not a test suite, that's a tax.&lt;/p&gt;

&lt;h3&gt;
  
  
  What had actually happened
&lt;/h3&gt;

&lt;p&gt;The agent had mocked almost everything. Every test built a stand-in for the objects it touched and set the fields inline, something like this, a hundred times over with small variations:&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_total_includes_tax&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tax_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;compute_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;110&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things made that fragile.&lt;/p&gt;

&lt;p&gt;First, the shape of the object was spelled out in every single test. So the day an order grew a new field, or a field changed type, I wasn't editing one place. I was editing every test that had ever built an order by hand. That is the pile going red, every time, for the same boring reason.&lt;/p&gt;

&lt;p&gt;Second, a lot of the tests weren't checking behavior at all. They were checking wiring: that this function called that function with these arguments. &lt;code&gt;assert_called_once_with(...)&lt;/code&gt;. Change how the pieces fit together, even when the result is identical, and those tests break.&lt;/p&gt;

&lt;p&gt;I had seen the same thing a few weeks earlier in a smaller form. Ids that were supposed to be one type kept coming back as another in the tests, and every reconciliation broke a fresh pile. Same root cause. The tests knew about details they never should have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the agent wrote them this way
&lt;/h3&gt;

&lt;p&gt;The agent did exactly what I asked. I asked for tests that pass. I got tests that pass. I never asked for tests that survive change, so it never optimized for that.&lt;/p&gt;

&lt;p&gt;Left alone, a coding agent does the least it can to get the tests passing. Mocking your own code is part of that: you skip building real data, and a lot of the time you just check that some function got called. It won't reach for a shared helper unless you tell it to, and it will happily agree that everything looks fine. That last part matters. These tools are agreeable by default. If you don't ask them to push back, they won't.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Two changes.&lt;/p&gt;

&lt;p&gt;One. Build the object in one place, and build the real thing. A single factory with sane defaults, each test overriding only the field it cares about.&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;make_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;defaults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&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;tax_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discount&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;# the real model, not a mock
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_total_includes_tax&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tax_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&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;compute_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a new field is one line in the factory, not a change to three hundred tests. And I stopped handing tests fake versions of my own objects. The factory builds the real thing, the same model the app uses. Mocks are only for the true edges now: the AI provider, the payment processor, email. For the code that actually reads and writes rows, the tests run against a real test database instead of a wall of mocks. When a model changes, the real constructors change with it, and most of the tests never notice.&lt;/p&gt;

&lt;p&gt;Two. Assert behavior, not calls. Check the number that came back, the row that landed in the database, the effect that actually happened. Not which function called which. This is the change that made the suite stop breaking on refactors, because the tests now cared only about outcomes, and outcomes didn't change when I moved the plumbing around.&lt;/p&gt;

&lt;p&gt;I deleted a lot of tests along the way too. A test that checks a mock returns the value you just set on the mock is not testing anything. The agent had written plenty, because they pass, and passing was the target. Cutting them made the suite smaller and the coverage number more honest.&lt;/p&gt;

&lt;h3&gt;
  
  
  The part that stuck
&lt;/h3&gt;

&lt;p&gt;The factories and the real database were mechanical. The fix that lasted was upstream, in how I set the work up in the first place. Two standing instructions went in and stayed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mock only the real edges of the system. Everywhere else, use real objects and check what actually happened, not which function called which.&lt;/li&gt;
&lt;li&gt;Challenge me. If I ask for something that's going to age badly, tell me before you do it, don't just do it because I asked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I could keep only one, it would be the second. Getting the agent to push back helped more than any factory did. Once it started telling me when I was about to make a mess, I made fewer of them.&lt;/p&gt;

&lt;p&gt;This one outgrew the tests. It's the same thing I learned running teams. Tell a dev team to build something a certain way and most of them build it. A few will stop and tell you it's wrong. An agent is more agreeable than any junior you've ever worked with, so it almost never will, unless you ask. Say it plainly: I want to be challenged. Then hold up your end. Listen, and either defend the call or drop it.&lt;/p&gt;

&lt;p&gt;This wasn't really a mocking problem. The agent built exactly what I asked for, and none of what I actually needed. I asked for tests that pass. What I wanted was tests that hold up when the code moves. That gap is mine to close, not the tool's. It cost me weeks of re-fixing the same tests before I stopped and dealt with the cause, so I'm writing it down for whoever hits it next.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>python</category>
      <category>tdd</category>
    </item>
  </channel>
</rss>
