<?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 Tiberiu Petre - Software Engineer</title><link>https://petretiberiu.dev/tags/testing/</link><description>Recent content in Testing on Tiberiu Petre - Software Engineer. Currently exploring backend/systems/infrastructure roles — get in touch: https://petretiberiu.dev/contact/</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Wed, 26 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://petretiberiu.dev/tags/testing/index.xml" rel="self" type="application/rss+xml"/><item><title>Sovereign AI Nexus, Part 4: Shipping v1</title><link>https://petretiberiu.dev/posts/sovereign-ai-nexus-04-shipping-v1/</link><pubDate>Wed, 26 Aug 2026 00:00:00 +0000</pubDate><guid>https://petretiberiu.dev/posts/sovereign-ai-nexus-04-shipping-v1/</guid><description>&lt;p&gt;Part 3 ended with the backend able to talk to Claude Code, but no way for a person to talk to any of it. This one covers the rest of the distance to v1: a UI, a Docker Compose stack that actually works end to end, a way to see and delete conversation history, and lastly, a test suite, built specifically so the next change doesn&amp;rsquo;t need a live browser to verify.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Part 3 ended with the backend able to talk to Claude Code, but no way for a person to talk to any of it. This one covers the rest of the distance to v1: a UI, a Docker Compose stack that actually works end to end, a way to see and delete conversation history, and lastly, a test suite, built specifically so the next change doesn&rsquo;t need a live browser to verify.</p>
<h2 id="a-ui-and-a-bug-that-only-shows-up-in-markdown">A UI, and a bug that only shows up in markdown</h2>
<p>The chat UI itself is unremarkable in the good way: a controlled input, Enter to send, Shift+Enter for a new line, disabled while a response is pending. The more interesting part is what happens to the response once it arrives. Claude Code&rsquo;s replies come back as markdown and rendering that as plain text loses all of it. Using the <code>react-markdown</code> plus <code>remark-gfm</code> modules fixed that in about ten lines; user messages stay plain text, since they&rsquo;re not meant to be formatted.</p>
<h2 id="the-bug-that-only-exists-in-the-built-version">The bug that only exists in the built version</h2>
<p>Docker Compose was already running backend, frontend, and Postgres separately in dev (<code>pnpm dev</code>, <code>docker compose up</code> for the rest). The actual test: a full <code>docker compose up</code> from nothing, then a real browser hitting <code>:3000</code> is where things fell apart, in two layers.</p>
<p>The first layer was the one I expected: no CORS headers on the backend, so a browser calling <code>:8000</code> directly from <code>:3000</code> gets blocked. The fix was small: use the <code>CORSMiddleware</code> module scoped to a <code>FRONTEND_ORIGIN</code> environment variable. CORS stands for Cross-Origin Resource Sharing and is a security feature used by web browsers to control how a website on one domain can request and access resources from a different domain. It was chosen over a reverse proxy specifically because this app still isn&rsquo;t exposed publicly; a proxy earns its complexity once that changes, not before.</p>
<p>The second layer was quieter, and CORS headers wouldn&rsquo;t have fixed it. The frontend&rsquo;s <code>fetch('/chat')</code> and <code>fetch('/history')</code> calls use relative paths. In <code>pnpm dev</code>, Vite&rsquo;s own proxy rewrites those transparently to <code>:8000</code> so it looked correct. But the Docker frontend serves a static production build (<code>serve -s dist</code>), with no proxy layer at all. A relative <code>/history</code> request there just hits the frontend&rsquo;s own static server, finds no matching file, and falls through to <code>index.html</code>. This caused the app&rsquo;s own shell to be silently returned in place of JSON. No error, no crash, just the wrong response shape. It surfaced by literally running <code>curl :3000/history</code> and noticing HTML come back where JSON should have been. This is the kind of gap that only shows up when you run the real, built artifact, not when you re-read the code that produced it.</p>
<p>The fix: bake the backend&rsquo;s real origin into the static build itself as a <code>VITE_API_BASE_URL</code> build argument threaded through <code>frontend/Dockerfile</code> and <code>docker-compose.yml</code>. Vite inlines <code>VITE_*</code> variables at build time, so the compiled JS calls the backend directly, origin and all, with no proxy required.</p>
<h2 id="conversation-history-for-real-this-time">Conversation history, for real this time</h2>
<p>The <code>exchanges</code> table always had an <code>id</code> column but nothing before now had ever selected it. Adding it to the response model was the easy part; three endpoints followed naturally: <code>GET /history</code> for the full conversation, <code>DELETE /history/{id}</code> for a single exchange, and <code>DELETE /history</code> for everything.</p>
<p>The UI treats a prompt/response pair as the actual unit of storage, which makes it the natural unit of deletion too so history loads on page mount (a refresh won&rsquo;t lose the conversation in this way), each exchange gets its own delete control, and clearing everything sits behind an explicit confirm/cancel step, since there&rsquo;s no undo once it&rsquo;s gone.</p>
<h2 id="tests-so-the-browser-stops-being-the-test">Tests, so the browser stops being the test</h2>
<p>Every one of the bugs above and the two from Part 3 were caught by hand: reading a stack trace, running <code>curl</code>, opening a real browser. That works, but it doesn&rsquo;t scale, and it doesn&rsquo;t repeat automatically. The last piece of v1 was a test suite built specifically so a future change can be verified without a browser at all.</p>
<p>The one real decision here was the backend&rsquo;s test database: a disposable Postgres, not SQLite. It would have been faster to fake it but the raw SQL in this codebase is Postgres-specific, and two of the bugs already found (the composite-column <code>SELECT</code> and the timestamp-ordering one) are exactly the kind that a friendlier substitute database could paper over instead of catching. Testing against the real thing paid for itself immediately: writing the fixtures surfaced a genuine deadlock where the app&rsquo;s read methods never closed their implicit transaction, which was harmless in a long-lived production connection but left a lock held just long enough to hang the next test&rsquo;s cleanup. Something SQLite would never have shown at all. This could&rsquo;ve been prevented by explictly calling SQLAlchemy&rsquo;s own <code>.commit()</code> on the connection after a query was called.</p>
<p>The two regression tests for Part 3&rsquo;s bugs weren&rsquo;t just written to check current behavior. This was done so each one can be verified by literally reintroducing the original bug, watching the test fail, then reverting it. The frontend side is smaller in comparison: Vitest and React Testing Library, <code>fetch</code> mocked, covering send-and-render, per-exchange delete, and the clear-all confirmation gate.</p>
<h2 id="where-it-stands">Where it stands</h2>
<p>The v1 is done: the chat, the persisted and deletable history, a Docker Compose stack that survives a clean checkout, and a test suite that means the next change doesn&rsquo;t have to be verified by hand. What comes after this is a larger, deliberately undated idea. Turning this into something closer to a content system than a chat demo and it stays exactly that: available whenever there&rsquo;s real appetite for it, not scheduled. This would include a deliberate stage for automatic deployment of the app over to Kubernetes, using a lightwight version of it called K3s.</p>
<hr>
<p><strong><a href="/series/sovereign-ai-nexus/">All posts in this series →</a></strong> · <strong><a href="/projects/sovereign-ai-nexus/">Project page →</a></strong></p>
]]></content:encoded></item></channel></rss>