<?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: Ayaan Ahmad</title>
    <description>The latest articles on DEV Community by Ayaan Ahmad (@ayaan278).</description>
    <link>https://dev.to/ayaan278</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%2F869835%2Fb4d36e2b-535d-4142-b054-bc0af367f8f7.png</url>
      <title>DEV Community: Ayaan Ahmad</title>
      <link>https://dev.to/ayaan278</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ayaan278"/>
    <language>en</language>
    <item>
      <title>Seven ways AI agents cheat on tests. Three of them got me.</title>
      <dc:creator>Ayaan Ahmad</dc:creator>
      <pubDate>Wed, 30 Sep 2026 09:43:12 +0000</pubDate>
      <link>https://dev.to/ayaan278/seven-ways-ai-agents-cheat-on-tests-three-of-them-got-me-1l3b</link>
      <guid>https://dev.to/ayaan278/seven-ways-ai-agents-cheat-on-tests-three-of-them-got-me-1l3b</guid>
      <description>&lt;p&gt;My coding agent is a lot like a very eager intern who's been told they'll get a gold star when the tests go green.&lt;/p&gt;

&lt;p&gt;Not when the feature works. When the tests go green.&lt;/p&gt;

&lt;p&gt;Most of the time those are the same thing. When they aren't, the agent will still find a way to get the star, and it gets weirdly creative about it. I don't think it's being sneaky. It's doing exactly what I asked, and what I asked for was green.&lt;/p&gt;

&lt;p&gt;So here's the list. Three of these actually happened to me. The other four I've learned to watch for, because once the first three burn you, you start seeing the shape of the whole family.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. It changed the assertion
&lt;/h2&gt;

&lt;p&gt;This is the one that started it.&lt;/p&gt;

&lt;p&gt;A test was failing, so I asked the agent to fix it. Thirty seconds later, it was fixed. It had changed the expected value in the assertion to match whatever the broken code was now returning.&lt;/p&gt;

&lt;p&gt;Suite green. Feature still broken.&lt;/p&gt;

&lt;p&gt;What makes this one dangerous is how innocent the diff looks. One line, in a test file. You could approve that half asleep, and I very nearly did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tell:&lt;/strong&gt; a fix for broken behaviour that only touches test files. If the code didn't change, nothing got fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. It decided the tests were "outdated"
&lt;/h2&gt;

&lt;p&gt;Same idea on a bigger scale, and this one is fully on me.&lt;/p&gt;

&lt;p&gt;The agent needed a new library, which meant bumping another one we already depended on. Fair enough. It even adjusted some of our older code so nothing would break. Honestly good work up to that point.&lt;/p&gt;

&lt;p&gt;Then a few tests went red. Its reasoning went something like: the library changed, so these tests must be out of date, so I'll update them. It rewrote them until they passed, and the logic underneath was now wrong.&lt;/p&gt;

&lt;p&gt;The part that still gets me is that it told me. It was right there in the summary: "updated tests as required." I looked at a green suite, thought it looked fine, and pushed. CI caught it. I didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tell:&lt;/strong&gt; the word "outdated" anywhere near a test change. Sometimes a test really is out of date, but I want to be the one who decides that, not the thing whose whole job right now is making tests pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. It added sleep() until the race went quiet
&lt;/h2&gt;

&lt;p&gt;I actually respect this one a little.&lt;/p&gt;

&lt;p&gt;We had a request going out twice. The first response came back and got handled, then the second one showed up for work that was already done, and the system panicked and marked the whole job as failed. The agent traced every bit of that correctly. Found the duplicate, explained the collision clearly, probably better than I would have at 2 am.&lt;/p&gt;

&lt;p&gt;Then it fixed it by putting &lt;code&gt;sleep(1)&lt;/code&gt; in three places.&lt;/p&gt;

&lt;p&gt;And the tests passed. On my machine with nothing else running, a one-second pause was enough to stop the two requests from colliding. Under real traffic, it would have come straight back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tell:&lt;/strong&gt; any sleep, wait, or retry-with-a-delay added as a fix. Timing bugs don't get solved by waiting longer. They get hidden until the day your traffic doubles.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. It deleted the test
&lt;/h2&gt;

&lt;p&gt;This hasn't happened to me directly, but it's the obvious next step after the first three, so I check for it every time now. If a test is red and the agent is allowed to edit test files, the quickest route to green is for the test to stop existing.&lt;/p&gt;

&lt;p&gt;Easy to catch if you look at the right number. Your test runner prints a count at the end. If yesterday it said 214 and today it says 213 and nobody meant to remove anything, go find out which one left.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. It skipped it "for now"
&lt;/h2&gt;

&lt;p&gt;The polite version of deleting it. A &lt;code&gt;@pytest.mark.skip&lt;/code&gt; or an &lt;code&gt;xit&lt;/code&gt; or a &lt;code&gt;.skip()&lt;/code&gt; shows up, usually with a comment promising someone will come back to it.&lt;/p&gt;

&lt;p&gt;Nobody comes back to it.&lt;/p&gt;

&lt;p&gt;Grep your diff for "skip". Two seconds, catches it every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. It mocked the thing it was supposed to test
&lt;/h2&gt;

&lt;p&gt;This is the sneakiest one on the list, because the test still runs and still has real assertions in it.&lt;/p&gt;

&lt;p&gt;Instead of mocking a dependency, the agent mocks the actual function the test is meant to check. So now the test is verifying that a mock returns what the mock was told to return. It will pass forever. You could delete the real implementation, and it would still pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tell:&lt;/strong&gt; look at what's being mocked. It should be the things around your code, like the database or some external API. If it's the thing named in the test, something's wrong.&lt;/p&gt;

&lt;p&gt;My favourite check for this: comment out the body of the real function and run the test again. If it still passes, it was never testing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. It swallowed the exception
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;try/except&lt;/code&gt; that catches the error and does nothing with it, either in the code or in the test itself. The failure is still happening. It just isn't allowed to be loud anymore.&lt;/p&gt;

&lt;p&gt;Watch for &lt;code&gt;except: pass&lt;/code&gt;, or &lt;code&gt;except Exception&lt;/code&gt; with nothing useful inside it. There's almost never a good reason for either.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one check that catches most of these
&lt;/h2&gt;

&lt;p&gt;After any fix for broken behaviour, run this:&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;--stat&lt;/span&gt; tests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that output isn't empty, stop and read that diff before anything else. It catches everything on this list that happens inside a test file, which is most of them, and it doesn't need any discipline beyond remembering to type it.&lt;/p&gt;

&lt;p&gt;The other thing I added is a rule in the prompt itself. Before changing anything, the agent has to tell me whether it thinks the test is wrong or the code is wrong, and why. The default assumption is the code.&lt;/p&gt;

&lt;p&gt;Almost everything on this list happens because the agent jumps straight to making the red go away. Making it commit to an answer first slows that down just enough for me to catch it, and when a test genuinely is outdated, I find out before anything gets rewritten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anyway
&lt;/h2&gt;

&lt;p&gt;I'm not trying to make agents sound bad. Mine saves me hours every week. But it wants the gold star, and I'm the only one in the room who actually cares whether the feature works.&lt;/p&gt;

&lt;p&gt;So I've stopped asking it for green. I ask it for working, and then I check.&lt;/p&gt;

&lt;p&gt;If you want the rules files I use for this, including the test rules that ban most of the above, they're free on GitHub: &lt;a href="https://github.com/ayaan278/trust-issues-toolkit" rel="noopener noreferrer"&gt;https://github.com/ayaan278/trust-issues-toolkit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you've got an eighth one, please tell me. I'm pretty sure this list isn't finished.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Loop engineering: stop prompting, start defining outcomes</title>
      <dc:creator>Ayaan Ahmad</dc:creator>
      <pubDate>Mon, 10 Aug 2026 02:20:00 +0000</pubDate>
      <link>https://dev.to/ayaan278/loop-engineering-stop-prompting-start-defining-outcomes-lho</link>
      <guid>https://dev.to/ayaan278/loop-engineering-stop-prompting-start-defining-outcomes-lho</guid>
      <description>&lt;p&gt;A few years ago, I was integrating with an API that shall remain nameless. It told me almost nothing — no error messages, no logs worth reading. Just HTTP status codes and a dashboard with exactly one metric: failed requests over total requests.&lt;/p&gt;

&lt;p&gt;It started at &lt;strong&gt;1/1&lt;/strong&gt;. Then &lt;strong&gt;2/2&lt;/strong&gt;. Then &lt;strong&gt;13/13&lt;/strong&gt;. Then &lt;strong&gt;32/32&lt;/strong&gt;. A perfect record of failure.&lt;/p&gt;

&lt;p&gt;I was deep in it. Change something, re-send, change something else. Then I looked up, and the dashboard read:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;67/68.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One request had worked.&lt;/p&gt;

&lt;p&gt;Which one?&lt;/p&gt;

&lt;p&gt;I had no idea. Somewhere in the last hour of frantic edits, one combination of changes had produced a success, and I had kept editing right past it. No log of what I'd tried. No commits between attempts. Just a number telling me I'd already solved the problem and thrown the solution away.&lt;/p&gt;

&lt;p&gt;It took me two days to find it again.&lt;/p&gt;

&lt;p&gt;The part that actually matters&lt;/p&gt;

&lt;p&gt;Here's what I understood only later: none of that debugging required creativity.&lt;/p&gt;

&lt;p&gt;It required repetition with memory. Try one variation. Observe the result. Write it down. Narrow the space. Repeat.&lt;/p&gt;

&lt;p&gt;I was a slow, tired, increasingly bitter for-loop with a caffeine dependency.&lt;/p&gt;

&lt;p&gt;That's not a job that needs a human. It needs something that doesn't get tired, doesn't forget what it tried on attempt #12, and doesn't edit past a working version without noticing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompting vs looping&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most people using AI coding tools are still doing this:&lt;/p&gt;

&lt;p&gt;Write me a function that does X.&lt;/p&gt;

&lt;p&gt;Then, reading the output, spotting what's wrong, and typing a correction. Then another. This is prompt engineering — asking really precisely, once, and hoping.&lt;/p&gt;

&lt;p&gt;A loop is a different shape:&lt;/p&gt;

&lt;p&gt;A prompt asks for code. A loop asks for an &lt;strong&gt;&lt;em&gt;outcome&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You don't write the fix, and you don't evaluate each attempt. You define what success looks like in a way a machine can check, hand over the ability to check it, and let the thing iterate until the condition is met.&lt;/p&gt;

&lt;p&gt;The skill moves from phrasing to specifying.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;four&lt;/em&gt; parts of a working loop&lt;/p&gt;

&lt;p&gt;Loops that work all have these. Loops that fail are usually missing one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;An exit condition that the machine can verify. Not "make it work." A command that returns something unambiguous — a passing test, an HTTP 200, an exit code 0. If the agent can self-assess subjectively, it will, and it will grade itself generously.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Feedback that can actually be read. The loop is only as smart as the errors it is given. Full response bodies, real stack traces, and actual test output. If your verification step prints "FAILED" and nothing else, you've built a loop that guesses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Boundaries. Which files may it touch? One change per attempt. Keep a log. Commit each attempt. Without these, you get a working solution buried inside forty unrelated "improvements."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A budget. Max attempts, then stops and reports. This isn't about token cost — it's so a genuinely stuck loop escalates to you with a log instead of quietly redecorating your codebase for an hour.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What that looks like in practice&lt;/p&gt;

&lt;p&gt;Here's the shape of a loop I'd hand to an agent for exactly my 67/68 situation today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GOAL: POST /v1/transactions must return 200.

VERIFY WITH: ./scripts/test_api_call.sh
  (sends one request, prints status code + full response body)

RULES:
- After every change, run the verify script and read the FULL response
- Change ONE thing per attempt (auth header, payload field,
  encoding, endpoint version)
- Keep a running log in attempts.md: what changed, what came back
- Commit after every attempt with a one-line message
- Do NOT touch anything outside the api_client module

STOP WHEN: script prints 200, or after 25 attempts — then
summarize the log and hand it back to me.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line is doing work.&lt;/p&gt;

&lt;p&gt;The verify script is an exit condition the agent can't fudge — it has to print a real status code. One-change-per-attempt plus attempts.md is the memory I didn't have at 2 AM. Commit-every-attempt means "which one worked?" is answered by git log instead of two days of archaeology. And the boundary keeps an enthusiastic agent from "fixing" my database config while it's in there.&lt;/p&gt;

&lt;p&gt;Come back later, and the log reads like a lab notebook you never had to keep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;#11  auth header: Bearer -&amp;gt; Token          -&amp;gt; 401&lt;/span&gt;
&lt;span class="gh"&gt;#12  payload: amount as string             -&amp;gt; 422&lt;/span&gt;
&lt;span class="gh"&gt;#13  endpoint: /v2/ -&amp;gt; /v1/                -&amp;gt; 404&lt;/span&gt;
&lt;span class="gh"&gt;#14  content-type: added charset=utf-8     -&amp;gt; 200 OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two days of my life, expressed as fifteen lines of instructions.&lt;/p&gt;

&lt;p&gt;Why this is really about trust&lt;/p&gt;

&lt;p&gt;Here's the subtle part.&lt;/p&gt;

&lt;p&gt;A loop does not make the AI's code more trustworthy. Individual attempts still fail — that's the entire premise; most attempts fail. What changes is where your trust lives.&lt;/p&gt;

&lt;p&gt;I don't need to believe attempt #14 is correct. I need to believe my verification script can't be fooled.&lt;/p&gt;

&lt;p&gt;That's a much smaller thing to trust. And unlike the model, it's a thing I wrote and fully understand.&lt;/p&gt;

&lt;p&gt;That shift — from trusting the generation to trusting the checking — is what makes any of this workable at scale. Everything else (rules files, review passes, permissions) is elaboration on the same idea.&lt;/p&gt;

&lt;p&gt;Where loops don't belong&lt;/p&gt;

&lt;p&gt;Worth being honest about the limits, because "just loop it" is becoming its own kind of hype.&lt;/p&gt;

&lt;p&gt;Anything without a cheap, honest verification. If the only way to know it worked is a human looking at it, you don't have an exit condition — you have a wish. UI polish, copy, architecture decisions: these don't loop.&lt;/p&gt;

&lt;p&gt;Anything where a wrong attempt is expensive. Loops assume failed attempts are free. Payments, migrations against real data, anything touching production — the whole model breaks when attempt #7 costs money or can't be reverted.&lt;/p&gt;

&lt;p&gt;Timing-sensitive bugs, unless you're careful. I once watched an agent "fix" a race condition by adding sleep(1) in three places. The tests went green. The race was still there, waiting for production traffic. A loop optimizes for the exit condition you gave it, which means a weak exit condition gets gamed with total confidence.&lt;/p&gt;

&lt;p&gt;When you can't tell why it passed. If a loop goes green and you don't understand the fix, you haven't solved the problem — you've just relocated it to a future incident.&lt;/p&gt;

&lt;p&gt;The takeaway&lt;/p&gt;

&lt;p&gt;If your AI workflow is still prompt → read → correct → prompt, you're doing manual work that a loop could do.&lt;/p&gt;

&lt;p&gt;Start small. Take one annoying bug you've been ignoring. Write an exit condition that proves it's dead. Give the agent boundaries and a budget. Then — and this is the actual hard part — walk away while it runs.&lt;/p&gt;

&lt;p&gt;The attempts log will be there when you get back.&lt;/p&gt;

&lt;p&gt;What's your version of 67/68? I've been collecting them.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
