<?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: Aleksandr Argunov</title>
    <description>The latest articles on DEV Community by Aleksandr Argunov (@argbetech).</description>
    <link>https://dev.to/argbetech</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%2F3790326%2Fdbb58c79-3080-4d9a-9a8e-710ca86c39b3.jpg</url>
      <title>DEV Community: Aleksandr Argunov</title>
      <link>https://dev.to/argbetech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/argbetech"/>
    <language>en</language>
    <item>
      <title>WebMCP vs MCP: when an agent should go through the browser</title>
      <dc:creator>Aleksandr Argunov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 13:47:00 +0000</pubDate>
      <link>https://dev.to/argbetech/webmcp-vs-mcp-when-an-agent-should-go-through-the-browser-4bec</link>
      <guid>https://dev.to/argbetech/webmcp-vs-mcp-when-an-agent-should-go-through-the-browser-4bec</guid>
      <description>&lt;p&gt;I've built a few WebMCP integrations, and I'm still not sure about it.&lt;/p&gt;

&lt;p&gt;The short version: WebMCP only makes sense to me when the agent is part of the browser, where you can watch and step in. When you work through chat, it feels unnatural. Why keep a browser involved when the agent can call the service directly? For a chat app, a desktop client, code or a job nobody watches, build a direct MCP connection. Shopify draws roughly the same line for its checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebMCP, browser-control tools and server MCP
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WebMCP&lt;/strong&gt; is a &lt;a href="https://github.com/webmachinelearning/webmcp" rel="noopener noreferrer"&gt;proposed web standard&lt;/a&gt;. The page registers tools, and the browser's built-in agent calls them, so the agent doesn't have to guess what a button does. Chrome runs it as an &lt;a href="https://developer.chrome.com/docs/ai/webmcp" rel="noopener noreferrer"&gt;origin trial from Chrome 149&lt;/a&gt;, with a flag for local development, and says the API may still change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser-control MCP tools&lt;/strong&gt; are MCP servers that operate a browser by clicking and typing. The site declares nothing, so the agent reads the page and works out what each element is for. Chrome calls this &lt;a href="https://developer.chrome.com/docs/ai/webmcp" rel="noopener noreferrer"&gt;actuation&lt;/a&gt; and describes WebMCP as the more reliable option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server MCP&lt;/strong&gt; means the service exposes its own tools, for example over MCP's &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http" rel="noopener noreferrer"&gt;Streamable HTTP transport&lt;/a&gt;, which streams with &lt;a href="https://argbe.tech/blog/sse-vs-websockets-ai-apps?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=webmcp-vs-mcp" rel="noopener noreferrer"&gt;SSE&lt;/a&gt;. No browser is involved. An MCP client such as ChatGPT Desktop or Claude Code calls the service directly, from a chat or from code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.chrome.com/docs/ai/webmcp/compare-mcp" rel="noopener noreferrer"&gt;Chrome's own comparison&lt;/a&gt; is clear that WebMCP doesn't replace MCP. WebMCP tools exist only while the page is open. An MCP server is there whenever a client calls it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I found building both
&lt;/h2&gt;

&lt;p&gt;The latest WebMCP integration I built let agents work on screenplay projects in a video web service, using the interface and the functions behind it.&lt;/p&gt;

&lt;p&gt;It worked. But I ended up replacing it with a direct MCP connection. I could control projects from ChatGPT Desktop or Claude Code without keeping the web interface in the loop. In my workflow, I found it faster, and it used fewer tokens. Without the browser, it was also much easier to build automated agent workflows in code. I haven't gone back to the WebMCP version.&lt;/p&gt;

&lt;p&gt;The automation part follows from the design: code can reach WebMCP tools only through a browser that has the page loaded. A server MCP has no page to keep open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shopify checkout: two ways in
&lt;/h2&gt;

&lt;p&gt;Shopify &lt;a href="https://shopify.dev/changelog/posts/webmcp-support-for-checkout" rel="noopener noreferrer"&gt;added WebMCP support to checkout on 2026-09-28&lt;/a&gt;. Browser agents can now read and update eligible checkouts, and submit them once the buyer confirms. Shopify's docs describe this and a server option side by side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkout WebMCP&lt;/strong&gt; works on the checkout open in the buyer's tab. It registers &lt;code&gt;get_checkout&lt;/code&gt;, &lt;code&gt;update_checkout&lt;/code&gt; and &lt;code&gt;complete_checkout&lt;/code&gt;, plus &lt;code&gt;navigate_to_storefront&lt;/code&gt; when the store has an online storefront. Before calling &lt;code&gt;complete_checkout&lt;/code&gt;, the agent has to show the buyer the order and total and get their permission. Shop Pay login and payment challenges stay with the buyer, on the page. (&lt;a href="https://shopify.dev/docs/agents/carts-and-checkout/checkout-webmcp" rel="noopener noreferrer"&gt;Checkout WebMCP reference&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The tools only appear on eligible checkouts. &lt;a href="https://shopify.dev/docs/agents/carts-and-checkout" rel="noopener noreferrer"&gt;Shopify lists the exceptions&lt;/a&gt;: B2B checkouts, embedded and mobile-SDK checkouts, checkouts with another shop's merchandise, draft orders, order edits, payment collection, and the standard three-page checkout unless the buyer uses Shop Pay. &lt;code&gt;update_checkout&lt;/code&gt; also leaves line items alone; the buyer changes those on the page. If you build on Shopify, check that list against your store's checkout before planning around WebMCP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkout MCP&lt;/strong&gt; runs from your agent's server and doesn't depend on the buyer's browser. It's a JSON-RPC endpoint at &lt;code&gt;https://{shop-domain}/api/ucp/mcp&lt;/code&gt;, and every request needs authentication or a signed request. When the buyer has to enter something or review the order, the agent hands them a checkout link to finish there. (&lt;a href="https://shopify.dev/docs/agents/carts-and-checkout/checkout-mcp" rel="noopener noreferrer"&gt;Checkout MCP reference&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Shopify's docs &lt;a href="https://shopify.dev/docs/agents/carts-and-checkout" rel="noopener noreferrer"&gt;recommend Checkout MCP&lt;/a&gt; unless the agent has to run in the buyer's browser. Either way, the buyer still approves the purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is heading
&lt;/h2&gt;

&lt;p&gt;Long term, I think agent-native will become the standard. The UI, WebMCP and direct MCP should all work with the same underlying logic and state. People and agents can then use whichever interface fits, without rebuilding the workflow for each one.&lt;/p&gt;

&lt;p&gt;Looks like Shopify is going this way too. Its WebMCP tools use &lt;a href="https://shopify.dev/changelog/posts/webmcp-support-for-checkout" rel="noopener noreferrer"&gt;the same state as the checkout UI&lt;/a&gt;, and both MCP options &lt;a href="https://shopify.dev/docs/agents/carts-and-checkout" rel="noopener noreferrer"&gt;use the same checkout object&lt;/a&gt;. That's the direction I'm most interested in.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A shorter version of this first appeared &lt;a href="https://www.linkedin.com/posts/aleksandr-argunov_shopify-webmcp-aiagents-share-7511125388266479616-jaWe" rel="noopener noreferrer"&gt;on LinkedIn&lt;/a&gt; on 2026-09-30. The maintained version, with the date I last checked Chrome's and Shopify's docs, is &lt;a href="https://argbe.tech/blog/webmcp-vs-mcp?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=webmcp-vs-mcp" rel="noopener noreferrer"&gt;on argbe.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webmcp</category>
      <category>mcp</category>
      <category>ai</category>
      <category>shopify</category>
    </item>
    <item>
      <title>SSE vs WebSockets for AI apps: ten web standards and two patterns</title>
      <dc:creator>Aleksandr Argunov</dc:creator>
      <pubDate>Thu, 22 Oct 2026 13:21:00 +0000</pubDate>
      <link>https://dev.to/argbetech/sse-vs-websockets-for-ai-apps-ten-web-standards-and-two-patterns-1cn1</link>
      <guid>https://dev.to/argbetech/sse-vs-websockets-for-ai-apps-ten-web-standards-and-two-patterns-1cn1</guid>
      <description>&lt;p&gt;Out of the box, the official OpenAI and Anthropic Python SDKs talk HTTP/1.1, a protocol from 1997 (&lt;a href="https://www.rfc-editor.org/info/rfc2068/" rel="noopener noreferrer"&gt;RFC 2068&lt;/a&gt;). Both build on HTTPX2, which leaves HTTP/2 off unless you turn it on (&lt;a href="https://developers.openai.com/api/reference/python" rel="noopener noreferrer"&gt;OpenAI SDK&lt;/a&gt;, &lt;a href="https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/_base_client.py" rel="noopener noreferrer"&gt;Anthropic SDK&lt;/a&gt;, &lt;a href="https://pydantic.dev/docs/httpx2/guides/http2/" rel="noopener noreferrer"&gt;HTTPX2&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Streaming answers? When they stream over HTTP, both APIs use Server-Sent Events, a W3C standard since 2015 (&lt;a href="https://developers.openai.com/api/docs/guides/streaming-responses" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;, &lt;a href="https://platform.claude.com/docs/en/build-with-claude/streaming" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;, &lt;a href="https://www.w3.org/standards/history/eventsource/" rel="noopener noreferrer"&gt;W3C&lt;/a&gt;). Live voice? OpenAI recommends WebRTC for browser and mobile clients (&lt;a href="https://developers.openai.com/api/docs/guides/voice-webrtc?voice-api=realtime" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;). Even MCP’s Streamable HTTP transport, which &lt;a href="https://argbe.tech/blog/webmcp-vs-mcp?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=sse-vs-websockets-ai-apps" rel="noopener noreferrer"&gt;remote MCP servers&lt;/a&gt; use, sends HTTP POST requests, with SSE for streaming. Since the 2026-07-28 revision, each POST gets back either JSON or its own SSE stream, and the old always-open GET stream is gone (&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Here are ten web standards and two patterns, with what each is good for in AI and where it falls short. Years mark standardization, not adoption. They don’t all compete: some are HTTP versions, others are transports, browser APIs or an authentication scheme, and several stack. HTTP/3 runs on QUIC (&lt;a href="https://www.rfc-editor.org/rfc/rfc9114.html" rel="noopener noreferrer"&gt;RFC 9114&lt;/a&gt;), and SSE works over any HTTP version.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://argbe.tech/blog/sse-vs-websockets-ai-apps?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=sse-vs-websockets-ai-apps" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxppxo9njkizjp44yc8em.webp" alt="How AI apps talk to servers: ten web standards and two patterns, from 1997 to 2026, each drawn as messages moving between your app and a server." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SSE or WebSockets?
&lt;/h2&gt;

&lt;p&gt;If only the server talks while an answer streams, SSE is often enough. Reach for WebSockets when both sides talk at once.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SSE&lt;/th&gt;
&lt;th&gt;WebSocket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direction&lt;/td&gt;
&lt;td&gt;Server to client&lt;/td&gt;
&lt;td&gt;Both ways&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;UTF-8 text&lt;/td&gt;
&lt;td&gt;Text and binary frames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs over&lt;/td&gt;
&lt;td&gt;An ordinary HTTP response&lt;/td&gt;
&lt;td&gt;Its own protocol, after an HTTP upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use it for&lt;/td&gt;
&lt;td&gt;Streamed answers, progress, notifications&lt;/td&gt;
&lt;td&gt;Live sessions where both sides talk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standardized&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://html.spec.whatwg.org/multipage/server-sent-events.html" rel="noopener noreferrer"&gt;WHATWG HTML&lt;/a&gt;, W3C since 2015&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc6455.html" rel="noopener noreferrer"&gt;RFC 6455&lt;/a&gt;, in 2011&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With SSE, the client still talks back, just with separate HTTP requests. Binary data has to be encoded as text first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick by job
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need to&lt;/th&gt;
&lt;th&gt;Start with&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Make one API or tool call at a time&lt;/td&gt;
&lt;td&gt;HTTP/1.1 is fine&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9112.html" rel="noopener noreferrer"&gt;RFC 9112&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream a model’s answer, progress or notifications&lt;/td&gt;
&lt;td&gt;SSE&lt;/td&gt;
&lt;td&gt;&lt;a href="https://html.spec.whatwg.org/multipage/server-sent-events.html" rel="noopener noreferrer"&gt;WHATWG HTML&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run a live session where both sides talk&lt;/td&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6455.html" rel="noopener noreferrer"&gt;RFC 6455&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add live voice or camera from a browser or phone&lt;/td&gt;
&lt;td&gt;WebRTC&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.w3.org/TR/webrtc/" rel="noopener noreferrer"&gt;W3C&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make several API or tool calls at once to one origin&lt;/td&gt;
&lt;td&gt;HTTP/2, or HTTP/3 on lossy networks&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc9113.html" rel="noopener noreferrer"&gt;RFC 9113&lt;/a&gt;, &lt;a href="https://www.rfc-editor.org/rfc/rfc9114.html" rel="noopener noreferrer"&gt;RFC 9114&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run a complex read-only search with a body&lt;/td&gt;
&lt;td&gt;HTTP QUERY, once every hop supports it&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc10008.html" rel="noopener noreferrer"&gt;RFC 10008&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mix must-arrive results with droppable live updates&lt;/td&gt;
&lt;td&gt;WebTransport&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.w3.org/TR/2026/CR-webtransport-20260730/" rel="noopener noreferrer"&gt;W3C CR&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show a site which agent is calling&lt;/td&gt;
&lt;td&gt;Web Bot Auth&lt;/td&gt;
&lt;td&gt;&lt;a href="https://datatracker.ietf.org/wg/webbotauth/documents/" rel="noopener noreferrer"&gt;IETF draft&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follow a long job&lt;/td&gt;
&lt;td&gt;Polling, or a webhook to your server&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://modelcontextprotocol.io/extensions/tasks/overview" rel="noopener noreferrer"&gt;MCP Tasks&lt;/a&gt;, &lt;a href="https://developers.openai.com/api/docs/guides/webhooks" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Established
&lt;/h2&gt;

&lt;h3&gt;
  
  
  HTTP/1.1
&lt;/h3&gt;

&lt;p&gt;1997 · Simple requests and responses, first specified in &lt;a href="https://www.rfc-editor.org/info/rfc2068/" rel="noopener noreferrer"&gt;RFC 2068&lt;/a&gt;. &lt;a href="https://www.rfc-editor.org/rfc/rfc9112.html" rel="noopener noreferrer"&gt;RFC 9112&lt;/a&gt; is the current version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; single API calls and tool calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; no multiplexing. Parallel requests usually need several connections, and pipelined responses still come back in order.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  WebSocket
&lt;/h3&gt;

&lt;p&gt;2011 · A two-way connection kept open, carrying text or binary frames (&lt;a href="https://www.rfc-editor.org/rfc/rfc6455.html" rel="noopener noreferrer"&gt;RFC 6455&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; live sessions where both sides talk, like server-side voice agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; for one-way text streaming, SSE is often enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  SSE
&lt;/h3&gt;

&lt;p&gt;2015 · Text pushed from server to client over HTTP (&lt;a href="https://html.spec.whatwg.org/multipage/server-sent-events.html" rel="noopener noreferrer"&gt;WHATWG HTML&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; streamed answers, progress updates and notifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; no binary messages, and no two-way traffic on the same stream.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  HTTP/2
&lt;/h3&gt;

&lt;p&gt;2015, updated 2022 · Many streams over one TCP connection. &lt;a href="https://www.rfc-editor.org/rfc/rfc9113.html" rel="noopener noreferrer"&gt;RFC 9113&lt;/a&gt; replaced the original &lt;a href="https://www.rfc-editor.org/rfc/rfc7540" rel="noopener noreferrer"&gt;RFC 7540&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; an agent making several API or tool calls at once to the same origin. Native gRPC uses it (&lt;a href="https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md" rel="noopener noreferrer"&gt;gRPC&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; one lost TCP packet can delay every stream on that connection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  QUIC
&lt;/h3&gt;

&lt;p&gt;2021 · Encrypted transport over UDP with independent streams (&lt;a href="https://www.rfc-editor.org/rfc/rfc9000.html" rel="noopener noreferrer"&gt;RFC 9000&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; a connection can survive a device changing networks, if the server allows it (&lt;a href="https://www.rfc-editor.org/rfc/rfc9000.html#section-9" rel="noopener noreferrer"&gt;§9&lt;/a&gt;). A lost packet holds up only the streams with data in it, which can be several, while the rest keep going (&lt;a href="https://www.rfc-editor.org/rfc/rfc9000.html#section-13" rel="noopener noreferrer"&gt;§13&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; some networks block UDP. Fallback is up to the app (&lt;a href="https://www.rfc-editor.org/rfc/rfc9114.html" rel="noopener noreferrer"&gt;RFC 9114&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  WebRTC
&lt;/h3&gt;

&lt;p&gt;2021 · Real-time audio, video and data. It first became a &lt;a href="https://www.w3.org/TR/webrtc/" rel="noopener noreferrer"&gt;W3C Recommendation&lt;/a&gt; in &lt;a href="https://www.w3.org/press-releases/2021/webrtc-rec/" rel="noopener noreferrer"&gt;January 2021&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; live voice and camera with a model, from a browser or phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; often unnecessary for text. It needs connection setup, and maybe a relay server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  HTTP/3
&lt;/h3&gt;

&lt;p&gt;2022 · HTTP over QUIC instead of TCP (&lt;a href="https://www.rfc-editor.org/rfc/rfc9114.html" rel="noopener noreferrer"&gt;RFC 9114&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; it can cut setup time and cross-stream delays on lossy networks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; making the model faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  New standard
&lt;/h2&gt;

&lt;h3&gt;
  
  
  HTTP QUERY
&lt;/h3&gt;

&lt;p&gt;June 2026 · Like GET, but with a request body. It’s safe and idempotent (&lt;a href="https://www.rfc-editor.org/rfc/rfc10008.html" rel="noopener noreferrer"&gt;RFC 10008&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; complex read-only searches like AI retrieval, with safe retries and caching where supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; relying on it before your whole stack supports it: client, CDN, proxies and server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Still developing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  WebTransport
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.w3.org/TR/2026/CR-webtransport-20260730/" rel="noopener noreferrer"&gt;W3C Candidate Recommendation&lt;/a&gt; · Reliable streams plus droppable datagrams. Major browsers have shipped it since March 2026; older ones and embedded webviews need a check (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebTransport" rel="noopener noreferrer"&gt;MDN&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; results that must arrive next to live updates that can go stale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; code that can’t absorb changes. The IETF protocol is still a draft (&lt;a href="https://datatracker.ietf.org/doc/draft-ietf-webtrans-http3/" rel="noopener noreferrer"&gt;IETF&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Web Bot Auth
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://datatracker.ietf.org/wg/webbotauth/documents/" rel="noopener noreferrer"&gt;IETF draft&lt;/a&gt; · Signed requests that show a site which agent is calling.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; telling real agents from bots posing as them. Cloudflare already checks these signatures (&lt;a href="https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; deciding what an agent may do. It proves identity, not &lt;a href="https://argbe.tech/blog/ai-coding-agent-security?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=sse-vs-websockets-ai-apps" rel="noopener noreferrer"&gt;permission&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Patterns, not protocols
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Polling
&lt;/h3&gt;

&lt;p&gt;Asking the server again and again whether something changed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; long AI jobs where checking every few seconds is fine, like MCP tasks. Where client and server support the &lt;a href="https://modelcontextprotocol.io/extensions/tasks/overview" rel="noopener noreferrer"&gt;Tasks extension&lt;/a&gt;, each job gets a handle to poll, and a client can pick it up again after reconnecting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; live updates. You either waste requests or lag behind.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Webhooks
&lt;/h3&gt;

&lt;p&gt;An HTTP request a service sends you when something happens.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good for:&lt;/strong&gt; telling your system that a batch run or agent task has finished. OpenAI, for one, sends them for batch and background jobs (&lt;a href="https://developers.openai.com/api/docs/guides/webhooks" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falls short:&lt;/strong&gt; they can’t reach a browser tab directly. Receiving them is a small integration of its own: a server that verifies signatures, handles duplicate deliveries and passes the event on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I first shared this list &lt;a href="https://www.linkedin.com/posts/aleksandr-argunov_aiagents-webdevelopment-softwareengineering-share-7518197240377634816-eqYw" rel="noopener noreferrer"&gt;on LinkedIn&lt;/a&gt; on 20 October 2026. The maintained version, with the animated diagram, every source and the date I last checked them, is &lt;a href="https://argbe.tech/blog/sse-vs-websockets-ai-apps?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=sse-vs-websockets-ai-apps" rel="noopener noreferrer"&gt;on argbe.tech&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>websockets</category>
      <category>http</category>
    </item>
    <item>
      <title>AI-generated unit tests: two patterns that pass with the bug, and a red-green fix</title>
      <dc:creator>Aleksandr Argunov</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:49:54 +0000</pubDate>
      <link>https://dev.to/argbetech/ai-generated-unit-tests-two-patterns-that-pass-with-the-bug-and-a-red-green-fix-11kh</link>
      <guid>https://dev.to/argbetech/ai-generated-unit-tests-two-patterns-that-pass-with-the-bug-and-a-red-green-fix-11kh</guid>
      <description>&lt;p&gt;Two tests can pass while &lt;code&gt;applyDiscount(100)&lt;/code&gt; returns 100 instead of the ticket's 90. One replaces the function with a mock. The other copies its boundary bug into the expected value.&lt;/p&gt;

&lt;p&gt;Agents love to create a thousand unit tests. All green. Then review smashes the whole thing.&lt;/p&gt;

&lt;p&gt;The agent already implemented a trash solution, and only then wrote generalized unit tests for it. Those tests describe the code it just wrote. So they all pass, bugs included.&lt;/p&gt;

&lt;p&gt;When an agent writes both the code and its tests, don't count the green checks. Look at where each expected value comes from. If it comes from the agent's code, the test can repeat the agent's mistake instead of catching it. If it comes from the ticket, the spec or a real example, and the test runs the real code and failed because the behavior was missing, it checks something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two patterns that pass with the bug
&lt;/h2&gt;

&lt;p&gt;Here are both in TypeScript with Vitest. Suppose an agent writes this for a discount ticket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pricing.ts. The ticket: orders of 100 or more get 10% off.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="c1"&gt;// bug: should be &amp;gt;=&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test that mocks the function it's supposed to test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// mocked.test.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;applyDiscount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./pricing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./pricing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gives 10% off at 100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// checks the mock, not the code&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test that calculates the expected value with the code's formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// same-formula.test.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;applyDiscount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./pricing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;applies the discount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="c1"&gt;// the code's own formula, bug included&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both pass, and the code still charges 100 for an order of exactly 100. With the mock, the real &lt;code&gt;applyDiscount&lt;/code&gt; never runs. Mocking a dependency, like a payment service, is a different and legitimate use (&lt;a href="https://abseil.io/resources/swe-book/html/ch13.html" rel="noopener noreferrer"&gt;Software Engineering at Google&lt;/a&gt;). Copying the formula copies the bug. Google's testing blog shows the same trap in &lt;a href="https://testing.googleblog.com/2014/07/testing-on-toilet-dont-put-logic-in.html" rel="noopener noreferrer"&gt;Don't Put Logic in Tests&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Researchers have measured a version of this. In a July 2026 study, 11 models in 13 model and mode setups wrote tests for 318 Java methods behind 233 real bugs. Averaged across those setups, 3.84% of the tests written from the buggy code passed on the bug and failed on the fix. Written from the fixed code, 0.46% did (&lt;a href="https://arxiv.org/html/2607.22883v1#S3.SS1" rel="noopener noreferrer"&gt;Zhao, Zhou and Cohen&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  More tests don't fix it
&lt;/h2&gt;

&lt;p&gt;In a study first posted in February 2026 on 500 SWE-bench Verified tasks, pushing agents to write more tests barely changed how many they solved (&lt;a href="https://arxiv.org/html/2602.07900v2#S5" rel="noopener noreferrer"&gt;Chen et al.&lt;/a&gt;). GPT-5.2 solved 359 tasks either way, and Gemini 3 Pro Preview went from 371 to 366. Telling Kimi K2 Thinking and DeepSeek v3.2 Reasoner not to write tests moved them from 317 to 304 and from 300 to 291. None of these changes was statistically significant.&lt;/p&gt;

&lt;p&gt;The cost depended on the model: GPT-5.2 made more API calls and used more tokens without solving more, and the two models told to skip tests used fewer (&lt;a href="https://arxiv.org/html/2602.07900v2#S5" rel="noopener noreferrer"&gt;results&lt;/a&gt;). It's one benchmark and one agent scaffold, mini-SWE-agent, and the study didn't test TDD or the workflow below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red and green slices, then review
&lt;/h2&gt;

&lt;p&gt;In my experience, TDD with small red and green slices, and reviewers after, works much better:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define what done means, in a way that can fail.&lt;/li&gt;
&lt;li&gt;Take the expected values from something the agent didn't write: the ticket, the spec, a real example.&lt;/li&gt;
&lt;li&gt;Write one failing test (red). It has to fail because the behavior is missing.&lt;/li&gt;
&lt;li&gt;Write only enough code to pass it (green).&lt;/li&gt;
&lt;li&gt;Next slice. Reviewers come in after green.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the discount, the test takes its expected value straight from the ticket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// spec.test.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;applyDiscount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./pricing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// From the ticket: an order of exactly 100 pays 90.&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gives 10% off an order of exactly 100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run against the agent's code, it fails red with &lt;code&gt;expected 100 to be 90&lt;/code&gt;. Change &lt;code&gt;&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;gt;=&lt;/code&gt; and it goes green. The mock test still passes, because it never ran the code. The formula test now fails: it was guarding the bug. Checked with Vitest 4.1.12: save the four blocks under their file names and run &lt;code&gt;npx vitest run&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mattpocock/skills/blob/3f599139104dfceabc7754eeb91319802f85e042/skills/engineering/tdd/SKILL.md" rel="noopener noreferrer"&gt;Matt Pocock's &lt;code&gt;/tdd&lt;/code&gt; skill&lt;/a&gt; is his version of this workflow. One slice at a time, and writing all the tests up front is an anti-pattern there. Refactoring belongs to review. That part is a choice: &lt;a href="https://newsletter.kentbeck.com/p/canon-tdd" rel="noopener noreferrer"&gt;Kent Beck's Canon TDD&lt;/a&gt; lets you refactor inside each cycle, after green.&lt;/p&gt;

&lt;p&gt;Lauren Tan (poteto), the author of &lt;a href="https://github.com/cursor/plugins/tree/eb4db2b55b2b7dec3d9282754cddf0a56bf4ff10/pstack" rel="noopener noreferrer"&gt;pstack&lt;/a&gt;, builds step 1 into it. Its &lt;a href="https://github.com/cursor/plugins/blob/ce649c03ac3b75b6ae80cbd15b7e2c536caedb5a/pstack/skills/figure-it-out/SKILL.md" rel="noopener noreferrer"&gt;&lt;code&gt;/figure-it-out&lt;/code&gt;&lt;/a&gt; skill asks the agent to state what done means, in a way that can fail, before it starts the run.&lt;/p&gt;

&lt;p&gt;TDD and &lt;code&gt;/figure-it-out&lt;/code&gt; can work magic when you understand what you are doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each check covers
&lt;/h2&gt;

&lt;p&gt;Even good unit tests are one row of this chart. Here's what every row covers. The When column is my rough rhythm, not a rule.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://argbe.tech/blog/ai-generated-unit-tests?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-generated-unit-tests" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgihw88c191r2rowqjok3.gif" alt="Animated chart titled What your 1,000 tests actually check. Ten rows show when each check runs and what it covers, across the screen, server and database of your app and an outside payment service. Unit, every edit: one function, alone. Mocks, inside unit tests: a fake stand-in for a real service. Smoke, every build: does it even start? Integration, every PR: do the parts work together? Regression, every PR: did the change break something old? Review, after green: a second pair of eyes, human or model. End-to-end, before release: a real user flow, start to finish. Security, before release: can someone get what isn't theirs? Mutation, rarely: plant a bug, see if any test notices. Set apart in gold at the bottom, Manual, by a human: actually uses it." width="600" height="750"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;What it covers&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit&lt;/td&gt;
&lt;td&gt;Every edit&lt;/td&gt;
&lt;td&gt;A small unit of behavior, run fast. The unit can be one function or a few that work together.&lt;/td&gt;
&lt;td&gt;&lt;a href="https://martinfowler.com/bliki/UnitTest.html" rel="noopener noreferrer"&gt;Fowler&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mocks&lt;/td&gt;
&lt;td&gt;In unit tests and narrow integration tests&lt;/td&gt;
&lt;td&gt;Stand-ins for real collaborators, like a payment service. A mock also checks how it was called.&lt;/td&gt;
&lt;td&gt;&lt;a href="https://martinfowler.com/bliki/TestDouble.html" rel="noopener noreferrer"&gt;Fowler&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smoke&lt;/td&gt;
&lt;td&gt;Every build&lt;/td&gt;
&lt;td&gt;Does it start, and do the critical basics work, before deeper testing?&lt;/td&gt;
&lt;td&gt;&lt;a href="https://glossary.istqb.org/en_US/term/smoke-testing" rel="noopener noreferrer"&gt;ISTQB&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;Every PR&lt;/td&gt;
&lt;td&gt;Do separately built parts work together once connected?&lt;/td&gt;
&lt;td&gt;&lt;a href="https://martinfowler.com/bliki/IntegrationTest.html" rel="noopener noreferrer"&gt;Fowler&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regression&lt;/td&gt;
&lt;td&gt;Every PR&lt;/td&gt;
&lt;td&gt;Did the change break something that used to work? Unit, integration and end-to-end tests can all do this job.&lt;/td&gt;
&lt;td&gt;&lt;a href="https://api.glossary.istqb.org/storage/help/R0uz58NqLzUz48LVUuyGSF76NFj4LHQazSs0GlNS.pdf" rel="noopener noreferrer"&gt;ISTQB&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;After green&lt;/td&gt;
&lt;td&gt;Someone other than the author reads the change: a person or a review model.&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://google.github.io/eng-practices/review/" rel="noopener noreferrer"&gt;Google&lt;/a&gt;, &lt;a href="https://docs.github.com/en/copilot/concepts/agent-capabilities/agent-powered-workflows-and-operations/code-review" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End-to-end&lt;/td&gt;
&lt;td&gt;Before release&lt;/td&gt;
&lt;td&gt;A whole user flow, start to finish, in production-like conditions.&lt;/td&gt;
&lt;td&gt;&lt;a href="https://glossary.istqb.org/en_US/term/end-to-end-testing" rel="noopener noreferrer"&gt;ISTQB&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Throughout development&lt;/td&gt;
&lt;td&gt;Whether the security controls hold. "Can someone get what isn't theirs?" is one question of many.&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://wstg.owasp.org/v4.2/2-Introduction/" rel="noopener noreferrer"&gt;OWASP&lt;/a&gt;, &lt;a href="https://wstg.owasp.org/v4.2/3-The_OWASP_Testing_Framework/0-The_Web_Security_Testing_Framework/" rel="noopener noreferrer"&gt;framework&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutation&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;td&gt;Change the code on purpose and see whether any test fails. A surviving change isn't always a real bug.&lt;/td&gt;
&lt;td&gt;&lt;a href="https://stryker-mutator.io/docs/" rel="noopener noreferrer"&gt;Stryker&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;By a human&lt;/td&gt;
&lt;td&gt;A person uses the product and checks that it behaves as expected.&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.atlassian.com/continuous-delivery/software-testing" rel="noopener noreferrer"&gt;Atlassian&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The maintained article, with the animated chart, source links and the last-checked date, is &lt;a href="https://argbe.tech/blog/ai-generated-unit-tests?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-generated-unit-tests" rel="noopener noreferrer"&gt;on argbe.tech&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>tdd</category>
      <category>typescript</category>
    </item>
    <item>
      <title>AI coding security: the tunnel my agent opened without asking</title>
      <dc:creator>Aleksandr Argunov</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:45:02 +0000</pubDate>
      <link>https://dev.to/argbetech/ai-coding-security-the-tunnel-my-agent-opened-without-asking-4ldn</link>
      <guid>https://dev.to/argbetech/ai-coding-security-the-tunnel-my-agent-opened-without-asking-4ldn</guid>
      <description>&lt;p&gt;In October 2026, Claude Code opened a Cloudflare tunnel on my machine. The Google Slides connector couldn't import my media files, so the agent put them on a public URL and let Google fetch them from there.&lt;/p&gt;

&lt;p&gt;It was a smart workaround, and it didn't ask for approval or warn me. The command just went by in its output, and I only caught it because I was reading along. While it ran, anyone with that link could reach those files on my machine.&lt;/p&gt;

&lt;p&gt;That was one session, with my settings. But most people won't be reading along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the agent wanted a public URL
&lt;/h2&gt;

&lt;p&gt;Google's Slides API inserts an image from a URL only if &lt;a href="https://developers.google.com/workspace/slides/api/reference/rest/v1/presentations/request#CreateImageRequest" rel="noopener noreferrer"&gt;the URL is publicly accessible&lt;/a&gt;. It fetches the image once, at insertion, and keeps its own copy. So the files only had to be public while Google fetched them.&lt;/p&gt;

&lt;p&gt;A tunnel is a quick way to get a public URL. &lt;a href="https://developers.cloudflare.com/tunnel/get-started/quick-tunnels/" rel="noopener noreferrer"&gt;Cloudflare's quick tunnels&lt;/a&gt; give a local service a temporary address, and by default anyone with that address can reach the service until the &lt;code&gt;cloudflared&lt;/code&gt; process stops.&lt;/p&gt;

&lt;p&gt;The workaround wasn't the problem. Doing it without asking was. Files only my machine could read became reachable from the internet, and the only notice was a line in the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check before an agent can expose anything
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Make the agent ask before anything that opens your machine. Tunnels, port forwarding, public share links and public buckets all turn something private into something reachable.&lt;/li&gt;
&lt;li&gt;Keep exposure as short as the job. If a service needs to fetch a file once, the tunnel can close right after; stopping &lt;code&gt;cloudflared&lt;/code&gt; ends access for everyone.&lt;/li&gt;
&lt;li&gt;Before an agent connects your email, files or calendar to an &lt;a href="https://argbe.tech/blog/webmcp-vs-mcp?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-coding-agent-security" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt;, check who wrote the server and what it can reach. MCP's &lt;a href="https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices" rel="noopener noreferrer"&gt;security best practices&lt;/a&gt; describe the authorization risks to look for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents in a business workflow need the same rule: a person approves anything that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Software isn't only prototyping
&lt;/h2&gt;

&lt;p&gt;Prototyping slop is solved. Pieter Levels calls it &lt;a href="https://levels.io/era-of-personal-software" rel="noopener noreferrer"&gt;"software is dead."&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;He's right that prototyping got cheap. But software isn't only prototyping. It takes time, effort, money and users before one approach is good enough for many people to reuse.&lt;/p&gt;

&lt;p&gt;One developer has been publishing &lt;a href="https://github.com/storytold/craft-launcher" rel="noopener noreferrer"&gt;the Craft apps&lt;/a&gt;, an early-alpha suite for image editing, documents, slides and more, each written from scratch in Rust. Even one of them, PhotoCraft, stands on hundreds of open source libraries: &lt;a href="https://github.com/storytold/photocraft/blob/1e2bea1060c3b281fea825cf367bc1317e3c75c5/Cargo.lock" rel="noopener noreferrer"&gt;its lockfile on 20 October&lt;/a&gt; listed 839 packages, counting its own crates and everything they pull in, and 803 of them came from crates.io. I don't think what a chatbot builds for you usually gets that far.&lt;/p&gt;

&lt;p&gt;I think most people will just ask for something and let the agent in the chat handle it. Chat apps can already connect to &lt;a href="https://support.claude.com/en/articles/10166901-use-google-workspace-connectors" rel="noopener noreferrer"&gt;files, email and calendars&lt;/a&gt;. For a lot of people the chat will be the only UI they use. They're about to own software and not know it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whose responsibility it is
&lt;/h2&gt;

&lt;p&gt;Now picture your agent wiring your email to an MCP server some stranger wrote. A &lt;a href="https://www.trendaisecurity.com/en-us/resources-insights/deep-research/stars-dont-save-you-popularity-is-not-security-in-the-mcp-ecosystem" rel="noopener noreferrer"&gt;June 2026 TrendAI study&lt;/a&gt; found confirmed security issues in 2,259 of 9,695 MCP servers, even after setting aside the ones flagged only for missing authentication. The servers came from public MCP directories crawled between November 2025 and March 2026.&lt;/p&gt;

&lt;p&gt;If that server leaks your inbox, that's on you. You connected it, even if the agent did it for you and you never noticed. It's the same thing as my tunnel, just with nobody reading the output.&lt;/p&gt;

&lt;p&gt;By "on you" I mean responsibility, not legal liability: someone has to know what's connected and what it can reach. Who is liable after a leak is a legal question. It depends on contracts, jurisdiction and the facts, and I'm not answering it here.&lt;/p&gt;

&lt;p&gt;Software isn't dying. More of it is moving to where people can't see it. And so is the responsibility for it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A shorter version of this first appeared &lt;a href="https://www.linkedin.com/posts/aleksandr-argunov_ai-aiagents-softwareengineering-share-7515678302993297408-GVN3" rel="noopener noreferrer"&gt;on LinkedIn&lt;/a&gt; on 2026-10-13. The maintained version, with every source and the date I last checked them, is &lt;a href="https://argbe.tech/blog/ai-coding-agent-security?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=ai-coding-agent-security" rel="noopener noreferrer"&gt;on argbe.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>claudecode</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
