<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>testing on S Anand</title>
    <link>https://www.s-anand.net/blog/tag/testing/</link>
    <description>Recent content in testing on S Anand</description>
    <generator>Hugo -- 0.156.0</generator>
    <language>en-us</language>
    <lastBuildDate>Sat, 14 Mar 2026 12:30:35 +0530</lastBuildDate>
    <atom:link href="https://www.s-anand.net/blog/tag/testing/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Hardening my Dev Container Setup</title>
      <link>https://www.s-anand.net/blog/hardening-my-dev-container-setup/</link>
      <pubDate>Sat, 14 Mar 2026 12:30:35 +0530</pubDate>
      <guid>https://www.s-anand.net/blog/hardening-my-dev-container-setup/</guid>
      <description>&lt;p&gt;&lt;img loading=&#34;lazy&#34; src=&#34;https://files.s-anand.net/images/2026-03-14-hardening-my-dev-container-setup.avif&#34;&gt; &lt;!-- https://gemini.google.com/app/9162b7c405fcd5ee --&gt;&lt;/p&gt;
&lt;p&gt;I run AI coding agents inside a &lt;a href=&#34;https://github.com/sanand0/scripts/blob/dbdf328bb0e960367d45f9f8580d921159be3e9e/dev.dockerfile&#34;&gt;Docker container&lt;/a&gt; for safety.&lt;/p&gt;
&lt;p&gt;The setup is&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/scripts/blob/dbdf328bb0e960367d45f9f8580d921159be3e9e/dev.dockerfile&#34;&gt;&lt;code&gt;dev.dockerfile&lt;/code&gt;&lt;/a&gt;: builds the image&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/scripts/blob/dbdf328bb0e960367d45f9f8580d921159be3e9e/dev.sh&#34;&gt;&lt;code&gt;dev.sh&lt;/code&gt;&lt;/a&gt;: launches the container with the right mounts and env vars&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/scripts/blob/dbdf328bb0e960367d45f9f8580d921159be3e9e/dev.test.sh&#34;&gt;&lt;code&gt;dev.test.sh&lt;/code&gt;&lt;/a&gt;: verifies everything works.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wrote them semi-manually and it had bugs. I had GitHub Copilot + GPT-5.4 High update tests and actually &lt;em&gt;run&lt;/em&gt; the commands to verify the setup.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what I learned from the process.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Make it easier to review&lt;/strong&gt;. The first run took long. I pressed &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;C&lt;/kbd&gt;, told Copilot to &amp;ldquo;add colored output, timing, and a live status line&amp;rdquo;. Then I re-ran. Instead of a bunch of &lt;code&gt;ERROR:&lt;/code&gt; lines, I now got a color-coded output with timing + a live status line showing what&amp;rsquo;s running.&lt;/p&gt;
&lt;pre&gt;&lt;div style=&#39;color: #cccccc; background-color: #000000; font-family: FiraCode Nerd Font, Maple Mono, Inconsolata, Consolas, monospace, monospace; font-size: 14px;&#39;&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;span style=&#39;color: #0dbc79;&#39;&gt;PASS&lt;/span&gt;&lt;span&gt;   2.495s  markitdown --help&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;span style=&#39;color: #cd3131;&#39;&gt;FAIL&lt;/span&gt;&lt;span&gt;   0.003s  playwright --version&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;span style=&#39;color: #ffbc03;&#39;&gt;note&lt;/span&gt;&lt;span&gt; /home/sanand/code/scripts/dev.test.sh: line 148: playwright: command not found&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;span style=&#39;color: #cd3131;&#39;&gt;FAIL&lt;/span&gt;&lt;span&gt;   0.003s  copilot version&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;span style=&#39;color: #ffbc03;&#39;&gt;note&lt;/span&gt;&lt;span&gt; /home/sanand/code/scripts/dev.test.sh: line 148: copilot: command not found&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Run in the target environment&lt;/strong&gt;. Tests reported that &lt;code&gt;fd&lt;/code&gt; and &lt;code&gt;node&lt;/code&gt; were missing, though they were &lt;em&gt;obviously&lt;/em&gt; in the image. Why? The test script was running on &lt;em&gt;my laptop&lt;/em&gt;, not in the container. It was checking the host, not the container. I didn&amp;rsquo;t realize that.&lt;/p&gt;
&lt;p&gt;Copilot added a check to see if we&amp;rsquo;re already inside a container:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;running_in_container&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt; -f /.dockerenv &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; grep -qaE &lt;span class=&#34;s1&#34;&gt;&amp;#39;(docker|containerd|kubepods)&amp;#39;&lt;/span&gt; /proc/1/cgroup 2&amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; ! running_in_container&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nb&#34;&gt;exec&lt;/span&gt; bash &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;SCRIPT_DIR&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/dev.sh&amp;#34;&lt;/span&gt; -- &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$SCRIPT_PATH&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; --inside-container
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;3. Check for conflicting commands&lt;/strong&gt;. &lt;code&gt;fd&lt;/code&gt; and &lt;code&gt;node&lt;/code&gt; were still missing. Why? &lt;code&gt;dev.sh&lt;/code&gt; was mounting my host &lt;code&gt;mise&lt;/code&gt; and &lt;code&gt;bin&lt;/code&gt; directories, overshadowing the image&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;-v &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/.local/share/mise:/home/vscode/.local/share/mise&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;-v &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/.local/bin:/home/vscode/.local/bin&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It doesn&amp;rsquo;t matter what&amp;rsquo;s installed in the container - only the host binaries are visible. Copilot removed the first line, and copied &lt;code&gt;mise&lt;/code&gt; into &lt;code&gt;~/.local/overrides/&lt;/code&gt; to override the &lt;code&gt;~/.local/bin&lt;/code&gt; PATH.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Use the right packages&lt;/strong&gt;. The default ImageMagick binary at &lt;code&gt;https://imagemagick.org/archive/binaries/magick&lt;/code&gt; is an AppImage. It doesn&amp;rsquo;t work in containers. So it hat to switch to &lt;code&gt;apt-get install -y imagemagick&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Use the right paths&lt;/strong&gt;. &lt;code&gt;npm install -g wscat playwright ...&lt;/code&gt; ran but to add them to the PATH, you need to &lt;code&gt;mise reshum node&lt;/code&gt;. Copilot added that.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;These are good practices, but since agents can fix them, they&amp;rsquo;re less important to learn.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vibe-coding good tests&lt;/strong&gt; enabled Copilot to fix them.&lt;br&gt;
&lt;strong&gt;Making output easy to read&lt;/strong&gt; enabled me to steer Copilot.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what I&amp;rsquo;d recommend if you&amp;rsquo;re trying to optimize setup / deployments.&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Mistakes AI Coding Agents Make</title>
      <link>https://www.s-anand.net/blog/mistakes-ai-coding-agents-make/</link>
      <pubDate>Sun, 22 Jun 2025 09:21:05 +0000</pubDate>
      <guid>https://www.s-anand.net/blog/mistakes-ai-coding-agents-make/</guid>
      <description>&lt;p&gt;&lt;img alt=&#34;Mistakes AI Coding Agents Make&#34; loading=&#34;lazy&#34; src=&#34;https://www.s-anand.net/blog/assets/image-9.webp&#34;&gt;&lt;/p&gt;
&lt;p&gt;I use &lt;a href=&#34;https://chatgpt.com/codex&#34;&gt;Codex&lt;/a&gt; to write tools while I walk. Here are merged PRs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/27&#34;&gt;Add editable system prompt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/23&#34;&gt;Standardize toast notifications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/25&#34;&gt;Persist form fields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/22&#34;&gt;Fix SVG handling in page2md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/19&#34;&gt;Add Google Tasks exporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/20&#34;&gt;Add Markdown table to CSV tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/24&#34;&gt;Replace simple alerts with toasts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/21&#34;&gt;Add CSV joiner tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/12&#34;&gt;Add SpeakMD tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This added technical debt. I spent four hours fixing the AI generated tests and code.&lt;/p&gt;
&lt;h3 id=&#34;what-mistakes-did-it-make&#34;&gt;What mistakes did it make?&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Inconsistency&lt;/strong&gt;. It flips between &lt;code&gt;execCommand(&amp;quot;copy&amp;quot;)&lt;/code&gt; and &lt;code&gt;clipboard.writeText()&lt;/code&gt;. It wavers on timeouts (50 ms vs 100 ms). It doesn&amp;rsquo;t always run/fix test cases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missed edge cases&lt;/strong&gt;. I &lt;a href=&#34;https://github.com/sanand0/tools/pull/25&#34;&gt;switched&lt;/a&gt; &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;. My earlier code didn&amp;rsquo;t have a &lt;code&gt;type=&amp;quot;button&amp;quot;&lt;/code&gt;, so clicks reloaded the page. It missed that. It also left scripts as plain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;script type=&amp;quot;module&amp;quot;&amp;gt;&lt;/code&gt; which was required.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited experimentation&lt;/strong&gt;. My failed with a HTTP 404 because the &lt;code&gt;common/&lt;/code&gt; directory wasn&amp;rsquo;t served. I added &lt;code&gt;console.log&lt;/code&gt;s to find this. Also, &lt;a href=&#34;https://www.npmjs.com/package/happy-dom&#34;&gt;happy-dom&lt;/a&gt; won&amp;rsquo;t handle multiple &lt;code&gt;export&lt;/code&gt;s instead of a single &lt;code&gt;export { ... }&lt;/code&gt;. I wrote code to verify this. Coding agents didn&amp;rsquo;t run such experiments.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;what-can-we-do-about-it&#34;&gt;What can we do about it?&lt;/h3&gt;
&lt;p&gt;Three things could have helped me:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Detailed coding rules&lt;/strong&gt;. E.g. &lt;strong&gt;always&lt;/strong&gt; run test cases and fix until they pass. Only use ESM. Always import from CDN via JSDelivr. That sort of thing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;100% test coverage&lt;/strong&gt;. Ideally 100% of code and all usage scenarios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log everything&lt;/strong&gt;. My tests got a HTTP 404 because I was not serving the &lt;code&gt;common/&lt;/code&gt; directory. LLMs couldn&amp;rsquo;t figure this out because it was not logged. Logging everything helps humans &lt;strong&gt;and&lt;/strong&gt; LLMs debug.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wait&lt;/strong&gt;. LLMs and coding agents keep improving. A few months down the line, they&amp;rsquo;ll run more experiments themselves.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Was AI coding worth the effort?&lt;/strong&gt; Here, yes. The tools &lt;strong&gt;worked&lt;/strong&gt;. Codex saved me 90% effort. My code quality obsession reduced savings to ~70%. Still huge.&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Turning Walks into Pull Requests</title>
      <link>https://www.s-anand.net/blog/turning-walks-into-pull-requests/</link>
      <pubDate>Fri, 30 May 2025 05:16:52 +0000</pubDate>
      <guid>https://www.s-anand.net/blog/turning-walks-into-pull-requests/</guid>
      <description>&lt;p&gt;&lt;img alt=&#34;Turning Walks into Pull Requests&#34; loading=&#34;lazy&#34; src=&#34;https://www.s-anand.net/blog/assets/jules.webp&#34;&gt;&lt;/p&gt;
&lt;p&gt;In the last few days, I&amp;rsquo;m coding with &lt;a href=&#34;https://jules.google.com/&#34;&gt;Jules&lt;/a&gt; (Google&amp;rsquo;s coding agent) while walking.&lt;/p&gt;
&lt;p&gt;Here are a few pull requests merged so far:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/5&#34;&gt;Add features via an issue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/saveform/pull/2&#34;&gt;Write test cases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/sanand0/tools/pull/8&#34;&gt;Add docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why bother?&lt;/strong&gt; My commute used to be audiobook time. Great for ideas, useless for deliverables. With ChatGPT, Gemini, Claude.ai, etc. I was able to have them write code, but I still needed to run, test, and deploy. &lt;a href=&#34;https://jules.google.com/&#34;&gt;Jules&lt;/a&gt; (and tools like &lt;a href=&#34;https://github.blog/news-insights/product-news/github-copilot-meet-the-new-coding-agent/&#34;&gt;GitHub Copilot Coding Agent&lt;/a&gt;, &lt;a href=&#34;https://openai.com/index/introducing-codex/&#34;&gt;OpenAI Codex&lt;/a&gt;, &lt;a href=&#34;https://github.com/qodo-ai/pr-agent&#34;&gt;PR Agent&lt;/a&gt;, etc. which are not currently free for everyone) lets you chat clone a repo, write code in a new branch, test it, and push. I can deploy that with a click.&lt;/p&gt;
&lt;p&gt;Fifteen minutes into yesterday&amp;rsquo;s walk I realised I&amp;rsquo;d shipped more code than in an hour at my desk (even with LLMs)!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;a href=&#34;https://jules.google.com/&#34;&gt;Jules&lt;/a&gt; via browser on phone, connect wired headset.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt&lt;/strong&gt; (by typing or speaking) the change to make. It reads the repo, creates a plan and writes code.&lt;/li&gt;
&lt;li&gt;It runs any existing test suites in a sandbox. Repeats until all tests pass.&lt;/li&gt;
&lt;li&gt;I have it publish a branch, go to &lt;a href=&#34;https://github.com/mobile&#34;&gt;GitHub Mobile&lt;/a&gt; and create a PR.&lt;/li&gt;
&lt;li&gt;Back home, I review the output and merge.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There are 3 kinds of uses I&amp;rsquo;ve put it to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#1. &lt;a href=&#34;https://github.com/sanand0/tools/pull/8&#34;&gt;Documentation&lt;/a&gt;&lt;/strong&gt; is the easiest. Low risk, high quality, boring task. Here&amp;rsquo;s a sample prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This repo has multiple directories, each with their own standalone single page application tools.&lt;/p&gt;
&lt;p&gt;If a directory does not have a README.md, add a concise, clear, USEFUL, tersely worded one covering what the tool does, the various real life use cases, and how it works.&lt;/p&gt;
&lt;p&gt;If a readme already exists, do NOT delete any information. Prefix this new information at the start.&lt;/p&gt;
&lt;p&gt;Avoid repeating information across multiple README files. Consolidated such information into the root directory readme.&lt;/p&gt;
&lt;p&gt;In the root directory README, also include links to each tool directory as a list, explaining in a single sentence what the tool does.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;#2. &lt;a href=&#34;https://github.com/sanand0/saveform/pull/2&#34;&gt;Testing&lt;/a&gt;&lt;/strong&gt; is the next best. Low risk, medium quality, boring task. Here&amp;rsquo;s an example:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Run the tests in this repo. Go through the code and see what parts of the code are not covered. Understand the logic and see what kinds of user scenarios are not covered. Add test cases to cover these in the same style as the existing code.&lt;/p&gt;
&lt;p&gt;Write MINIMAL, ELEGANT code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;#3. &lt;a href=&#34;https://github.com/sanand0/tools/pull/5&#34;&gt;Coding&lt;/a&gt;&lt;/strong&gt; may not be the best suited for this. High risk, medium quality, and interesting. But here&amp;rsquo;s a sample prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Fix &lt;a href=&#34;https://github.com/sanand0/tools/issues/3&#34;&gt;https://github.com/sanand0/tools/issues/3&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Allow the user to enter just the GitHub @username, e.g. @sanand0 apart from the URL&lt;/li&gt;
&lt;li&gt;Add crisp documentation at the start explaining what the app does&lt;/li&gt;
&lt;li&gt;Only display html_url (as a link), avatar_url (as an image), name, company, blog, location, email, hireable, bio, twitter_username, public_repos, public_gists, followers, following, created_at, updated_at&lt;/li&gt;
&lt;li&gt;Format dates like Wed 28 May 2025. Format numbers with commas. Add links to blog, twitter_username, email&lt;/li&gt;
&lt;li&gt;Add &amp;ldquo;Download CSV&amp;rdquo; and &amp;ldquo;Copy to Excel&amp;rdquo; buttons similar to the json2csv/ tool&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Automated tests are a great way to reduce AI coding risk, as &lt;a href=&#34;https://simonwillison.net/2025/May/28/automated-tests/&#34;&gt;Simon Willison suggests&lt;/a&gt;. I need to do more of this!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wins &amp;amp; Losses&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Good&lt;/strong&gt;: 1 walk = one merged PR. Even with LLMs, it used to take me 2 hours. Now, it&amp;rsquo;s about half an hour of reclaimed walking &amp;ldquo;dead time&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Good&lt;/strong&gt;: Test-first prompting caught a sneaky race condition I’d have missed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bad&lt;/strong&gt;: Told Jules “add docs” without saying “don’t overwrite existing.” It politely destroyed my README. Manual revert ensued.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bad&lt;/strong&gt;: Front-end tasks need visual QA; I&amp;rsquo;m still hunting for a zero-setup UAT preview on mobile.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The industry echoes the pattern: GitHub’s new Copilot agent submits draft PRs behind branch protections [1]; Sweep auto-fixes small tickets but can over-touch files [2]; Microsoft’s own engineers found agents flailed on complex bug fixes [3].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;But…&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isn’t this risky?&lt;/strong&gt; Maybe. Branch protections, CI, and human review stay intact. Agents are like a noisy junior devs who never sleep.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is the diff readable?&lt;/strong&gt; If not, I have it retry, write more reviewable diffs, and explain clearly in comments &amp;amp; commit messages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does it have enough context?&lt;/strong&gt; I add all the context clearly in the issue or the prompt. That can take some research.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security?&lt;/strong&gt; The agents run inside repos you give it access. Prompt injection and exfiltration &lt;strong&gt;are&lt;/strong&gt; possible risks, but &lt;strong&gt;only&lt;/strong&gt; if it accesses external code / websites.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;How to start&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pick a low-stakes repo with solid tests.&lt;/li&gt;
&lt;li&gt;Pick an agent. &lt;a href=&#34;https://jules.google.com/&#34;&gt;Jules&lt;/a&gt; has 5 tasks/day free for now. Or pay and use &lt;a href=&#34;https://github.blog/news-insights/product-news/github-copilot-meet-the-new-coding-agent/&#34;&gt;GitHub Copilot Coding Agent&lt;/a&gt;, &lt;a href=&#34;https://openai.com/index/introducing-codex/&#34;&gt;OpenAI Codex&lt;/a&gt;, etc. Or self-host &lt;a href=&#34;https://github.com/qodo-ai/pr-agent&#34;&gt;PR Agent&lt;/a&gt;, etc.&lt;/li&gt;
&lt;li&gt;Write a failing test.&lt;/li&gt;
&lt;li&gt;Go for a walk and talk.&lt;/li&gt;
&lt;li&gt;Merge (or laugh) on return.&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    <item>
      <title>Leaning into the power of AI coding</title>
      <link>https://www.s-anand.net/blog/leaning-into-the-power-of-ai-coding/</link>
      <pubDate>Wed, 16 Oct 2024 03:55:59 +0000</pubDate>
      <guid>https://www.s-anand.net/blog/leaning-into-the-power-of-ai-coding/</guid>
      <description>&lt;p&gt;&lt;img alt=&#34;Leaning into the power of AI coding&#34; loading=&#34;lazy&#34; src=&#34;https://www.s-anand.net/blog/assets/DALL%C2%B7E-2024-10-16-11.52.50-A-colorful-single-panel-comic-strip-in-the-style-of-classic-Calvin-Hobbes.-Calvin-a-young-boy-with-wild-hair-is-wobbling-precariously-on-a-bike-as.webp&#34;&gt;&lt;/p&gt;
&lt;p&gt;Yesterday (15 Oct 2024), I used &lt;a href=&#34;https://www.cursor.com/&#34;&gt;Cursor&lt;/a&gt; to code more than I ever have. (Doing&#39;s how we learn, I guess. Not just reading.)&lt;/p&gt;
&lt;svg overflow=&#34;hidden&#34; viewbox=&#34;0 0 1697 1288&#34; width=&#34;500&#34; xml:space=&#34;preserve&#34; xmlns=&#34;http://www.w3.org/2000/svg&#34;&gt;&lt;path d=&#34;M32 18h1621v107H32z&#34; fill=&#34;#C0E6F5&#34;&gt;&lt;/path&gt;&lt;path d=&#34;M521 130h92v82h-92zM521 232h163v82H521zM521 334h526v82H521zM521 437h97v81h-97zM521 539h5v81h-5zM521 641h255v81H521zM521 743h143v81H521zM521 845h342v81H521zM521 946h92v82h-92zM521 1049h169v81H521zM521 1151h1122v81H521z&#34; fill=&#34;#FFB628&#34;&gt;&lt;/path&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;700&#34; transform=&#34;translate(47.4 96)&#34;&gt;Date&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;700&#34; transform=&#34;translate(526.35 96)&#34;&gt;Usage&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 198)&#34;&gt;05&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.46 198)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.27 198)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.34 198)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.4 198)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(1566.77 198)&#34;&gt;15&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 300)&#34;&gt;06&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.46 300)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.27 300)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.34 300)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.4 300)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(1566.77 300)&#34;&gt;27&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 402)&#34;&gt;07&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.5 402)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.32 402)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.38 402)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.44 402)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(1566.77 402)&#34;&gt;87&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 504)&#34;&gt;08&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.46 504)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.27 504)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.34 504)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.4 504)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(1566.77 504)&#34;&gt;16&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 606)&#34;&gt;09&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.46 606)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.27 606)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.34 606)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.4 606)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 708)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.46 708)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.27 708)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.34 708)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.4 708)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(1566.77 708)&#34;&gt;42&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 810)&#34;&gt;11&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.46 810)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.27 810)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.34 810)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.4 810)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(1566.77 810)&#34;&gt;24&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 912)&#34;&gt;12&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.46 912)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.27 912)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.34 912)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.4 912)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(1566.77 912)&#34;&gt;57&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 1014)&#34;&gt;13&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.46 1014)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.27 1014)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.34 1014)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.4 1014)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(1566.77 1014)&#34;&gt;15&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 1117)&#34;&gt;14&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.46 1117)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.27 1117)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.34 1117)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.4 1117)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(1566.77 1117)&#34;&gt;28&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(47.4 1219)&#34;&gt;15&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(126.46 1219)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(145.27 1219)&#34;&gt;10&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(224.34 1219)&#34;&gt;-&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(248.4 1219)&#34;&gt;2024&lt;/text&gt;&lt;text font-family=&#34;Aptos Narrow,Aptos Narrow_MSFontService,sans-serif&#34; font-size=&#34;78&#34; font-stretch=&#34;semi-condensed&#34; font-weight=&#34;400&#34; transform=&#34;translate(1530.68 1219)&#34;&gt;186&lt;/text&gt;&lt;path d=&#34;M511 18h5v1h-5z&#34; fill=&#34;#E0E0E0&#34;&gt;&lt;/path&gt;&lt;path d=&#34;M32.5 120.5h1621&#34; fill=&#34;none&#34;&gt;&lt;/path&gt;&lt;path d=&#34;M32 120h1621v5H32z&#34; fill=&#34;#44B3E1&#34;&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;p&gt;This was mainly to create and publish 2 libraries on npm over 6 hours:&lt;/p&gt;
&lt;ol class=&#34;wp-block-list&#34;&gt;
&lt;li&gt;&lt;a href=&#34;https://www.npmjs.com/package/asyncsse&#34;&gt;asyncsse&lt;/a&gt; - which converts a &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events&#34;&gt;Server-Sent Event&lt;/a&gt; stream into an async iterator that I can use in a &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of&#34;&gt;for await ... of&lt;/a&gt; loop&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.npmjs.com/package/asyncllm&#34;&gt;asyncllm&lt;/a&gt; - which standardizes the Server-Sent Events streamed by the popular LLMs into an easy to use form.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This exercise broke several mental barriers for me.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Writing in a new language&lt;/strong&gt;. &lt;a href=&#34;https://deno.com/blog/v2.0&#34;&gt;Deno 2.0&lt;/a&gt; was released recently. I was impressed by the compatibility with npm packages. Plus, it&#39;s a single EXE download that includes a linter, tester, formatter, etc. Like all recent cool fast tools, it&#39;s written in Rust. So I decided to use it for testing. Running &lt;code&gt;deno test&lt;/code&gt; runs the &lt;a href=&#34;https://github.com/gramener/asyncsse/blob/main/test.js&#34;&gt;entire test suite&lt;/a&gt;. My prompts included asking it to:&lt;/p&gt;
&lt;ul class=&#34;wp-block-list&#34;&gt;
&lt;li&gt;&lt;code&gt;Create a Deno HTTP server to mock requests for the tests&lt;/code&gt;. This is cool because a single, simple code chunk runs the server within the test suite.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Serve static files from samples/&lt;/code&gt; to move my tests into files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Writing test cases&lt;/strong&gt;. Every line of this code was written by Cursor via Claude 3.5 Sonnet. &lt;em&gt;Every line&lt;/em&gt;. My prompt was, &lt;code&gt;Look at the code in @index.js and write test cases for scenarios not yet covered.&lt;/code&gt; It&#39;s surprising how much of the SSE spec it already knew, and anticipated edge cases like:&lt;/p&gt;
&lt;ul class=&#34;wp-block-list&#34;&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/gramener/asyncsse/blob/13b33140f6bc17618789b5075a96f8f85a84c605/test.js#L67-L69&#34;&gt;SSE values might have a colon&lt;/a&gt;. I learnt for the first time that the &lt;code&gt;limit&lt;/code&gt; parameter in &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split&#34;&gt;String.split()&lt;/a&gt; is very different from Python&#39;s str.split. (The splits, then picks the first few, ignoring the rest. Python ensures the rest is packed into the last split.) This helped me find a major bug.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/gramener/asyncsse/blob/13b33140f6bc17618789b5075a96f8f85a84c605/test.js#L52-L54&#34;&gt;SSE has comments&lt;/a&gt;. Empty keys are treated as strings. Didn&#39;t know this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I was able to use it to generate test cases based on content as well. &lt;code&gt;Based on @index.js and @&lt;a href=&#34;https://github.com/gramener/asyncllm/blob/main/samples/openai.txt&#34;&gt;openai.txt&lt;/a&gt; write a test case that verifies the functionality&lt;/code&gt;  created the entire &lt;a href=&#34;https://github.com/gramener/asyncllm/blob/cbde11155f3a8567794058d01da1bd8d72f996b7/test.js#L25-L34&#34;&gt;test case for OpenAI responses&lt;/a&gt;. (I did have to edit it because LLMs don&#39;t count very well, but it was minimal.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bridging test coverage gaps&lt;/strong&gt;. The prompt that gave me the most delightful result was &lt;code&gt;Are there any scenarios in @index.js not tested by @test.js?&lt;/code&gt; It did a great job of highlighting that I hadn&#39;t covered Groq, Azure, or CloudFlare AI workers (though they were mentioned in the comments), error handling, empty/null values in some cases, tested for multiple tool calls. I had it generate mock test data for some of these and added the tests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Enhancing knowledge with references&lt;/strong&gt;. I passed Cursor the SSE documentation via &lt;code&gt;@https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events&lt;/code&gt; and asked it to find more scenarios my code at &lt;code&gt;@index.js&lt;/code&gt; had not covered. This found a number of new issues.&lt;/p&gt;
&lt;ul class=&#34;wp-block-list&#34;&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/gramener/asyncsse/blob/13b33140f6bc17618789b5075a96f8f85a84c605/test.js#L79-L82&#34;&gt;SSE values can be split across multiple lines&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/gramener/asyncsse/blob/13b33140f6bc17618789b5075a96f8f85a84c605/test.js#L75-L78&#34;&gt;Fields without values are valid&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/gramener/asyncsse/blob/13b33140f6bc17618789b5075a96f8f85a84c605/test.js#L71-L73&#34;&gt;Empty lines should be ignored&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Generating bindings&lt;/strong&gt;. I avoid TypeScript because I don&#39;t know it. Plus, it requires an compilation step for the browser. But TypeScript bindings are helpful. So I prompted Cursor, using the Composer (which can create new files) to &lt;code&gt;Create TypeScript bindings for @index.js in index.d.ts&lt;/code&gt; - which id did almost perfectly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check for errors&lt;/strong&gt;. I typed &lt;code&gt;Check this file for errors&lt;/code&gt; on @&lt;a href=&#34;https://github.com/gramener/asyncsse/blob/main/index.d.ts&#34;&gt;index.d.ts&lt;/a&gt;. I don&#39;t know enough to figure this out. It went through the description and said everything seems fine. But I saw a TypeScript plugin error that said, &lt;code&gt;Property &#39;data&#39; of type &#39;string | undefined&#39; is not assignable to &#39;string&#39; index type &#39;string&#39;.ts(2411)&lt;/code&gt;. When prompted, it spotted the issue. (The earlier code assumed all properties are strings. But some can be undefined too. It fixed it.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Documentation&lt;/strong&gt;. At first, I asked the Composer to &lt;code&gt;Create a README.md suitable for a world-class professional open source npm package&lt;/code&gt; and it did a &lt;a href=&#34;https://github.com/gramener/asyncsse/blob/main/README.md&#34;&gt;pretty good job&lt;/a&gt;. I just needed to update the repository name. I further prompted it to &lt;code&gt;Modify README based on @index.js and share examples from @test.js&lt;/code&gt; on &lt;a href=&#34;https://github.com/gramener/asyncllm/blob/main/README.md&#34;&gt;asyncllm&lt;/a&gt;, which did an excellent job.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code review&lt;/strong&gt;. I asked it to &lt;code&gt;Review this code. Suggest possible improvements for simplicity, future-proofing, robustness, and efficiency&lt;/code&gt; and it shared a few very effective improvements.&lt;/p&gt;
&lt;ol class=&#34;wp-block-list&#34;&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookahead_assertion&#34;&gt;Regex lookaheads&lt;/a&gt; for efficient regular expression splitting, i.e. use &lt;code&gt;buffer.split(/(?=\r?\n\r?\n)/)&lt;/code&gt; instead of &lt;code&gt;buffer.split(/(\r?\n\r?\n)/)&lt;/code&gt; -- and though I haven&#39;t tested this, it looked cool.&lt;/li&gt;
&lt;li&gt;Restructuring complex if-else code into &lt;a href=&#34;https://github.com/gramener/asyncllm/blob/cbde11155f3a8567794058d01da1bd8d72f996b7/index.js#L64-L84&#34;&gt;elegant parsers&lt;/a&gt; that made my code a lot more modular.&lt;/li&gt;
&lt;li&gt;Error handling. It added &lt;code&gt;try {} catch {}&lt;/code&gt; blocks at a few places that helped catch errors that I don&#39;t anticipate but don&#39;t hurt.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Code simplification&lt;/strong&gt;. Several times, I passed it a code snippet, saying just &lt;code&gt;Simplify&lt;/code&gt;. Here&#39;s an example:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;events&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;event&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;of&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;asyncLLM&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(...))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;events&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;push&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This can be simplified to&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;events&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fromAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;asyncLLM&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(...))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Packaging&lt;/strong&gt;. I copied a &lt;code&gt;package.json&lt;/code&gt; from an earlier file and asked it to &lt;code&gt;Modify package.json, notable keywords and files and scripts based on @index.js&lt;/code&gt; which it did a &lt;a href=&#34;https://github.com/gramener/asyncllm/blob/main/package.json&#34;&gt;perfect job of&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blogging&lt;/strong&gt;. I wrote this blog post with the help of the chat history on Cursor. Normally, such blog posts take me 3-4 hours. This one took 45 minutes. I just had to pick and choose from history. (I lost a few because I renamed directories. I&#39;ll be careful not to do that going forward.)&lt;/p&gt;
&lt;hr class=&#34;wp-block-separator has-alpha-channel-opacity&#34;/&gt;
&lt;p&gt;Overall, it was a day of great learning. Not in the classroom sense of &#34;Here&#39;s something I didn&#39;t know before&#34;, but rather the cycling / swimming sense of &#34;Here&#39;s something I now know to do.&#34;&lt;/p&gt;
</description>
    </item>
  </channel>
</rss>
