<?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: Rahul</title>
    <description>The latest articles on DEV Community by Rahul (@crypticsaiyan).</description>
    <link>https://dev.to/crypticsaiyan</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%2F3752742%2F62ffe1d2-b8c1-426a-a3fa-dcc67eac0b89.jpg</url>
      <title>DEV Community: Rahul</title>
      <link>https://dev.to/crypticsaiyan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/crypticsaiyan"/>
    <language>en</language>
    <item>
      <title>Red, then green: 11 bugs TestSprite found in my NASA video research agent</title>
      <dc:creator>Rahul</dc:creator>
      <pubDate>Mon, 26 Oct 2026 23:39:32 +0000</pubDate>
      <link>https://dev.to/crypticsaiyan/red-then-green-11-bugs-testsprite-found-in-my-nasa-video-research-agent-15</link>
      <guid>https://dev.to/crypticsaiyan/red-then-green-11-bugs-testsprite-found-in-my-nasa-video-research-agent-15</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ephemeris is a research agent over 87 NASA clips. I put 85 TestSprite tests behind it (34 browser, 51 API), and they found 11 real bugs.&lt;/li&gt;
&lt;li&gt;The centre of the story is PR #13: the first commit is only a regression test and goes red, the second carries the fixes and goes green.&lt;/li&gt;
&lt;li&gt;Every pull request gets a preview of its own and a TestSprite check before it can merge. Below: how I set that up, what the tests caught, and the steps to copy it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is my write-up for TestSprite Season 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: an archive in the wrong order
&lt;/h2&gt;

&lt;p&gt;NASA published almost nothing online before the year 2000. So a clip uploaded in 2015 is routinely discussing results from 1976. Sort the video library by its own metadata and you get the order things were posted, which is not the order anything happened.&lt;/p&gt;

&lt;p&gt;That matters if you want to understand how a field changed its mind. The real value of an archive like this is somebody on camera in 1965 saying what they thought Mars was, and being wrong, and somebody in 2015 saying what we know now. Titles and tags do not capture that. The truth is in the spoken words and in what was printed on the screen behind them.&lt;/p&gt;

&lt;p&gt;Today you can find a clip called "Mars report". You cannot ask how our understanding of water on Mars changed and get back the exact moments that answered it. That is what I built Ephemeris to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ephemeris does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsv5tvmgd8mrlovsyaqv7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsv5tvmgd8mrlovsyaqv7.png" alt="App" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You ask a question in plain English and get three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A cited answer, ordered by when events actually happened&lt;/strong&gt;, not by when NASA uploaded the file. Every claim carries a citation number that maps to a moment in a clip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An evidence reel&lt;/strong&gt;, one video cut from the exact seconds that support the answer, with the citation number, the year, how that year was determined, the mission and the NASA identifier burned into the frame so it survives an export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 3D solar system&lt;/strong&gt; whose camera follows the reel. Each moment stands on the body it concerns, and the camera moves through the decades as the reel plays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The corpus is 87 real NASA public-domain clips: 240 minutes, 1,484 scenes, 1957 to 2025. Nothing is synthetic and nothing is mocked. The live app is at &lt;a href="https://ephemeris-taupe.vercel.app" rel="noopener noreferrer"&gt;https://ephemeris-taupe.vercel.app&lt;/a&gt;, and the code is at &lt;a href="https://github.com/crypticsaiyan/Ephemeris" rel="noopener noreferrer"&gt;https://github.com/crypticsaiyan/Ephemeris&lt;/a&gt;. The four preset questions on the landing page are real pipeline output and need no key.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works, briefly
&lt;/h2&gt;

&lt;p&gt;The tests only make sense if you know what they are guarding, so here is the short version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Teaching the archive to read
&lt;/h3&gt;

&lt;p&gt;The footage is ingested into VideoDB, which gives each clip a chained analyzer graph. Spoken words and on-screen text (OCR) both feed a vision-language pass that describes each scene while reading what was said and printed during it. Fusion happens at ingestion, not as a merge of three searches at query time. The OCR is load-bearing: NASA footage carries burned-in lower-thirds and mission clocks, and where narration is sparse it is the only textual signal there is.&lt;/p&gt;

&lt;p&gt;The result is five indexes: &lt;code&gt;transcript&lt;/code&gt; (what was said), &lt;code&gt;scene_semantic&lt;/code&gt; (what is visible), &lt;code&gt;scene_facets&lt;/code&gt; (what kind of scene it is), &lt;code&gt;ocr&lt;/code&gt; (what is printed on screen) and &lt;code&gt;mission_meta&lt;/code&gt; (who, where and when). One vision-language pass backs two of them, so the second surface costs no extra inference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four ways to know a date
&lt;/h3&gt;

&lt;p&gt;Publication date is useless for this archive, so every scene carries a date and a record of how it was known:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Trust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scene&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the scene states a year, in speech or on screen&lt;/td&gt;
&lt;td&gt;highest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mission&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the stated year was impossible for the mission, so its operating window decided&lt;/td&gt;
&lt;td&gt;corrected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;video&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the clip as a whole is anchored to an era&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NASA's publication date&lt;/td&gt;
&lt;td&gt;always correct, rarely interesting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The correction matters. A compilation dated once at the top hands that year to every scene beneath it, and one Curiosity segment came back as 1990, twenty-one years before launch. Across the archive, 20.7% of dated scenes with a known mission fell outside its operating window, and 15.7% of the archive is re-dated. A date the scene stated itself is never overruled, because archive footage is full of retrospect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Answering a question
&lt;/h3&gt;

&lt;p&gt;A search box takes a query and returns clips. Ephemeris takes a question, decomposes it into sub-questions and alternate phrasings, and runs two gates before any retrieval: is the question answerable at all, and does the corpus hold the world it names. Then it retrieves from each index separately, joins runs of touching cells into passages, caps the evidence at two moments per clip, snaps each window to sentence boundaries using word-level timings, orders the result by the era each moment discusses, and asks the model for a cited answer with a chronology and caveats. Last, it compiles the reel.&lt;/p&gt;

&lt;p&gt;Several of those choices were forced by measurement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query expansion is load-bearing.&lt;/strong&gt; "Twin rovers landing in 2004" retrieves nothing, because the archive says "the first of two rovers". That phrasing hits rank 1 at 0.783.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexes are queried one at a time.&lt;/strong&gt; Scores are not comparable across indexes, and a combined call dropped a 0.6899 transcript hit out of 20 results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diversity is enforced.&lt;/strong&gt; Without a per-clip cap the chronology comes from one dense clip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refusing is a feature.&lt;/strong&gt; Given gibberish, an early version split it into plausible sub-questions and answered with confidence. Asked about Venus, it once reasoned from Artemis reentry footage while conceding no clip showed Venus. There are zero Venus scenes, and the coverage gate now ends that run in 19 seconds with what the archive does cover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retrieval is checked against ground truth built from NASA's own published caption files, so the evaluation does not grade the pipeline on its own output: 35 of 36 cases, mean recall 0.909.&lt;/p&gt;

&lt;h3&gt;
  
  
  The scene that follows the reel
&lt;/h3&gt;

&lt;p&gt;In the browser, one &lt;code&gt;activeEvidenceIndex&lt;/code&gt; is the entire sync bus. Inline &lt;code&gt;[n]&lt;/code&gt; markers, timeline needles, shot rows, the craft standing in the 3D scene and the reel's playhead all read from it, so clicking any one of them moves all of them. That single shared index is also why so much of the interface is testable: if one thing selects a moment, you can assert that the other four followed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making every pull request testable
&lt;/h2&gt;

&lt;p&gt;The first change was architecture, not tests.&lt;/p&gt;

&lt;p&gt;The app ran as one container, and the Next.js route spawned the Python agent as a child process. That rules out a preview deployment per pull request, and a pull request check should test the pull request's own code, not &lt;code&gt;main&lt;/code&gt;. In &lt;a href="https://github.com/crypticsaiyan/Ephemeris/pull/7" rel="noopener noreferrer"&gt;PR #7&lt;/a&gt; I split the Python agent into its own small HTTP service on Render's free tier, behind a shared token, and moved the web app to Vercel. The web route now validates, rate-limits, and forwards the question to the service, streaming the events back unchanged. Now every pull request gets a Vercel preview with its own URL, and that URL is what TestSprite tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up TestSprite
&lt;/h2&gt;

&lt;p&gt;This is what I actually did, in order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Generate a suite from the live app.&lt;/strong&gt; TestSprite explores the URL with several agents and proposes a test plan. Mine proposed 62 tests across 8 use cases, with a lot of repetition: "switch between presets" appeared five times and "chronology is in order" three. I trimmed it to 20. A pull request run costs roughly one credit per browser test, so a deliberate suite is also a cheaper one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Write assertions about what a user can see.&lt;/strong&gt; I uploaded a short product requirements document first, with the real labels, the preset names, and what counts as a pass. Then I edited the generated steps so each test passes for a reason. The reel test, for example, passes if the player plays, or says the stream is slow, or says the browser cannot play it, with a link; it fails only on a bare "loading" spinner forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Run it once before wiring it up.&lt;/strong&gt; A trigger re-runs an existing suite. If the suite has never been generated and run, the trigger has nothing to start. Generate it, run it, and only then add the trigger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Connect the GitHub App and add a trigger.&lt;/strong&gt; The pull request trigger listens for the Vercel commit status, so it fires when the preview is live, not when the build starts. Its target URL pattern is the preview hostname with a branch placeholder, for example &lt;code&gt;https://my-app-git-{branch-slug}-my-team.vercel.app&lt;/code&gt;. Then switch on the merge gate, so a red check blocks the merge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Drive it from the command line.&lt;/strong&gt; The TestSprite CLI lets me read a failure the same way I would read a stack trace. &lt;code&gt;testsprite test failure summary &amp;lt;id&amp;gt;&lt;/code&gt; prints the failing step and a root-cause hypothesis. &lt;code&gt;testsprite test plan put &amp;lt;id&amp;gt;&lt;/code&gt; replaces a test's steps from a file. That made the loop quick: read the failure, decide whether the app or the test is wrong, fix one of them, push.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Add an API project.&lt;/strong&gt; A second TestSprite project plans its tests from &lt;code&gt;docs/openapi.yaml&lt;/code&gt; and from probes of the live API. It started with 7 tests and grew to 51, and runs against production through a push trigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pull request that proves it: PR #13
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/crypticsaiyan/Ephemeris/pull/13" rel="noopener noreferrer"&gt;PR #13&lt;/a&gt; is built so the history proves the tests can fail. I had written 26 new tests and ran them against production before touching any code. Two failed for real reasons. So the pull request opens with a commit that contains only a regression test, and the fixes come in the second commit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Commit&lt;/th&gt;
&lt;th&gt;Python CI&lt;/th&gt;
&lt;th&gt;TestSprite browser suite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1: regression test only&lt;/td&gt;
&lt;td&gt;red&lt;/td&gt;
&lt;td&gt;16 of 18 pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2: the fixes&lt;/td&gt;
&lt;td&gt;green&lt;/td&gt;
&lt;td&gt;18 of 18 pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9k92tcotn4641xbxn7l2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9k92tcotn4641xbxn7l2.png" alt="Testsprite" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I left the red run in the history on purpose. A test you have never seen fail is a test you cannot fully trust, and a red run followed by a green one is the cheapest evidence that it is real. TestSprite rewrites its comment on the pull request as each run finishes, but the red run stays on the first commit's check, which links to its report.&lt;/p&gt;

&lt;p&gt;Three bugs were behind the red:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ask service corrupted the next request after a refusal.&lt;/strong&gt; The service checks the auth token before it reads the request body, on purpose, so an unauthorised caller costs nothing. That meant a 401 went out with the JSON body still unread. On a connection that stays open, those bytes are parsed as the next request line, and a proxy that reuses upstream connections hands that failure to a different caller. TestSprite's API test sends three refusals in a row and hung on the second. The service now closes the connection after any refusal, and a Python regression test fails without that line. That test is why commit 1 turns Python CI red as well as TestSprite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Venus and Mercury panels said "reading corpus summary" forever.&lt;/strong&gt; The 3D scene lets you focus either planet, but the corpus has no footage of them, so the summary file has no entry for those bodies. The panel could not tell "no footage" from "still loading". Now a loaded summary with no entry for that body says there is no footage of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first question after the free Render host slept failed with a 502.&lt;/strong&gt; The service sleeps when idle and takes about 40 seconds to wake, and the route gave up on the first 502. I had only ever tested it warm. The route now retries 502, 503, 504 and refused connections for up to 75 seconds, and the trace tells the visitor the host is starting up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Climbing from 13 to 18: PR #8
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/crypticsaiyan/Ephemeris/pull/8" rel="noopener noreferrer"&gt;PR #8&lt;/a&gt; is the other one worth reading, because the suite went 13 of 18, 16 of 18, then 18 of 18 across three commits, and every step was a real fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The planet names could not be clicked.&lt;/strong&gt; The names floating over the 3D scene were plain text sitting over an invisible picker sphere, so clicking a name did nothing. I turned them into buttons. The test then failed again, differently: the buttons were visible but no click reached them. I reproduced it in headless Chrome. The button was there, the click timed out, and &lt;code&gt;elementFromPoint&lt;/code&gt; at its centre returned the wrapper, not the button. The label rule set &lt;code&gt;pointer-events: none&lt;/code&gt; so floating names never block a drag on the scene, and the buttons inherited it. A more specific rule fixed it, and a direct check showed the body panel opening with its counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typing stalled.&lt;/strong&gt; Two tests were blocked because the page was too slow to type into. I measured it. On a browser drawing the 3D scene on the CPU, the orrery at device pixel ratio 1.75 cost 53 ms a frame, and every keystroke in the Ask box waited behind a frame, about 110 ms a key. The scene now checks once whether the renderer is software and, if so, draws at half resolution without antialiasing: 17 ms a frame, about 25 ms a key. I had never noticed it on a machine with a GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A browser without H.264 sat on "loading stream" forever.&lt;/strong&gt; The player now says it cannot play the reel, or that the stream is slow, and gives a link. A spinner that never ends is the worst of the three possible outcomes, and the test treats it as a failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Asked list vanished when empty&lt;/strong&gt;, so a first visit showed no sign of where runs go. An empty row now says so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An over-limit question was refused only after submit.&lt;/strong&gt; The page now counts characters live, disables submit and says "question too long".&lt;/p&gt;

&lt;h2&gt;
  
  
  The others
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A value that vanished&lt;/strong&gt; (&lt;a href="https://github.com/crypticsaiyan/Ephemeris/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt;). A long value set from code disappeared from the Ask box. It was a controlled React input, and React discards a value that arrives without an input event, which is how some assistive tools, password managers and test drivers fill a field. The box is now uncontrolled, and the length is read from the field on submit, so an over-limit question is refused however it got there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 500 where a 400 belonged&lt;/strong&gt; (&lt;a href="https://github.com/crypticsaiyan/Ephemeris/pull/16" rel="noopener noreferrer"&gt;PR #16&lt;/a&gt;). &lt;code&gt;/api/ask&lt;/code&gt; answered 500 with an empty body when the question was a number, an array, an object or a boolean, because the route called &lt;code&gt;.trim()&lt;/code&gt; on whatever the JSON held. A visitor cannot cause it from the page, but any API client can. I wrote one API test per JSON type and ran them against production first, so the red run is in the portal's history. The route now answers 400 "question must be text", before the quota is touched.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A suite that grew from 24 to 85
&lt;/h2&gt;

&lt;p&gt;I started with 24 tests. By the end there were 85, and the growth came from asking what each area promises:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;What is asserted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser, presets and answers&lt;/td&gt;
&lt;td&gt;34 in total&lt;/td&gt;
&lt;td&gt;each preset loads its own answer, chronology and reel; citations select their evidence row; the timeline ruler has a needle per moment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser, scene&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;focusing a body opens its panel; the camera strip flips from following the reel to manual orbit; hide notes clears the side panels and show notes restores them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser, live ask&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;one real question through the whole agent, saved to the browser, surviving a reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API, preset files&lt;/td&gt;
&lt;td&gt;51 in total&lt;/td&gt;
&lt;td&gt;every preset is a complete result: eight evidence rows in ascending era order, one reel shot per row, citations that point at rows, a numbered trace, a reachable stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API, refusals&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;invalid JSON, empty, whitespace-only, over-length and non-text questions each get the exact documented 400, as JSON, with no path or key in the body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API, ask service&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;health check; no token or a wrong token is a 401; no other routes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API, the site&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;the page contains no key names or service address&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Alongside those, eight offline Python suites run in CI on every push, plus the browser-side checks. The one test that spends VideoDB credits is the live ask, and the visitor quota of five questions an hour bounds it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Give every pull request a preview of its own.&lt;/strong&gt; Splitting the agent into a service is the change that made a pull request check meaningful, because the tests ran against the pull request's code and not against &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the regression test as its own first commit.&lt;/strong&gt; Seeing the red run before the fix, as in PR #13, is how I know the test is real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert what a user can see, and let the test fail on a missing explanation.&lt;/strong&gt; "Plays, or says why not" is a stronger contract than "plays", because it forbids the silent spinner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure before you fix.&lt;/strong&gt; The 53 ms a frame came from a measurement, not a hunch, and the fix could be checked against it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run new tests against production before you fix anything.&lt;/strong&gt; The red run is the evidence that the test would have caught the bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep branch names short.&lt;/strong&gt; The Vercel preview hostname is built from the branch name and is limited to 63 characters. A longer name will not match the URL pattern the trigger expects, so the check never starts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Copy this setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Make sure every pull request gets its own preview URL, and that the app on it can run without a local backend.&lt;/li&gt;
&lt;li&gt;Write a short requirements document with the real labels and what counts as a pass, and generate a suite from the live URL.&lt;/li&gt;
&lt;li&gt;Trim duplicates, edit the steps, and run the suite once by hand.&lt;/li&gt;
&lt;li&gt;Add a pull request trigger on the Vercel commit status, with the preview hostname as the target pattern, and turn the merge gate on.&lt;/li&gt;
&lt;li&gt;Open a pull request that contains only a failing test, watch it go red, then add the fix and watch it go green.&lt;/li&gt;
&lt;li&gt;Plan an API project from your OpenAPI file, and run new tests against production before you fix what they find.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live app: &lt;a href="https://ephemeris-taupe.vercel.app" rel="noopener noreferrer"&gt;https://ephemeris-taupe.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/crypticsaiyan/Ephemeris" rel="noopener noreferrer"&gt;https://github.com/crypticsaiyan/Ephemeris&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #13, red then green: &lt;a href="https://github.com/crypticsaiyan/Ephemeris/pull/13" rel="noopener noreferrer"&gt;https://github.com/crypticsaiyan/Ephemeris/pull/13&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #8, suite 13/18 to 18/18: &lt;a href="https://github.com/crypticsaiyan/Ephemeris/pull/8" rel="noopener noreferrer"&gt;https://github.com/crypticsaiyan/Ephemeris/pull/8&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #11, vanishing value: &lt;a href="https://github.com/crypticsaiyan/Ephemeris/pull/11" rel="noopener noreferrer"&gt;https://github.com/crypticsaiyan/Ephemeris/pull/11&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #16, 500 instead of 400: &lt;a href="https://github.com/crypticsaiyan/Ephemeris/pull/16" rel="noopener noreferrer"&gt;https://github.com/crypticsaiyan/Ephemeris/pull/16&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #7, splitting out the ask service: &lt;a href="https://github.com/crypticsaiyan/Ephemeris/pull/7" rel="noopener noreferrer"&gt;https://github.com/crypticsaiyan/Ephemeris/pull/7&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a Real-Time Multilingual Dubbing Platform and Used TestSprite MCP to Test It</title>
      <dc:creator>Rahul</dc:creator>
      <pubDate>Fri, 17 Apr 2026 19:54:14 +0000</pubDate>
      <link>https://dev.to/crypticsaiyan/i-built-a-real-time-multilingual-dubbing-platform-and-used-testsprite-mcp-to-test-it-4dm4</link>
      <guid>https://dev.to/crypticsaiyan/i-built-a-real-time-multilingual-dubbing-platform-and-used-testsprite-mcp-to-test-it-4dm4</guid>
      <description>&lt;p&gt;What if you could speak, and everyone listening heard you in their own language, with no noticeable delay?&lt;/p&gt;

&lt;p&gt;That question turned into PolyDub.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Vomt_04eN-Q"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Three modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Broadcast:&lt;/strong&gt; one speaker, listeners worldwide, each hearing a dubbed stream in their language&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multilingual Rooms:&lt;/strong&gt; everyone speaks their own language, everyone hears everyone else in theirs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VOD Dubbing:&lt;/strong&gt; upload a video, download a dubbed MP4 with SRT subtitles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real-time pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mic -&amp;gt; WebSocket -&amp;gt; Deepgram Nova-2 (STT) -&amp;gt; Google Translate (~300ms) -&amp;gt; Deepgram Aura-2 (TTS) -&amp;gt; Speaker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perceived latency is around 1.2 to 1.5 seconds. Fast enough for a real conversation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzk608kfzbmhsrfz819wk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzk608kfzbmhsrfz819wk.png" alt="Landing Page" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  A Few Decisions Worth Explaining
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why Google Translate instead of Lingo.dev for real-time?&lt;/strong&gt; Lingo.dev is LLM-based, which means 5 to 8 seconds of latency. Fine for batch work, not for live speech. Google's &lt;code&gt;gtx&lt;/code&gt; endpoint runs at 250 to 350ms warm. Lingo.dev is still in the project, compiling UI strings at build time across 15 locales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Deepgram Aura-2?&lt;/strong&gt; Aura v1 only shipped English voices regardless of the language param. Aura-2 ships genuinely native-accent voices: Japanese prosody, Spanish regional variation, German intonation. Using an English voice mispronouncing another language defeats the entire product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a per-listener TTS queue?&lt;/strong&gt; In a room with multiple speakers, audio chunks from different people arrive at the same socket in parallel. Without serialization they interleave into noise. A per-socket promise chain fixes this, and the queue depth is capped at 1 so stale utterances get dropped rather than building an 8-second backlog.&lt;/p&gt;




&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa4jxm91s4u06cx1yzqfo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa4jxm91s4u06cx1yzqfo.png" alt="Broadcast setup" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Broadcast mode: pick source and target languages, hit Start, share the listener link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fefodwxc32ngxtmo580ui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fefodwxc32ngxtmo580ui.png" alt="Room view" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rooms: each participant sets their own language and voice. The server handles translation per-person.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xchu1x0ppk05vnz0028.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xchu1x0ppk05vnz0028.png" alt="VOD studio" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;VOD: upload a video, pick a language, get a dubbed MP4 and SRT file back.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing With TestSprite MCP
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrfblh1yqgk1t4c71e5v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrfblh1yqgk1t4c71e5v.png" alt=" " width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project was built under hackathon pressure. Third-party APIs can fail in specific ways. Frontend validation is easy to break quietly. Writing full test coverage by hand would have eaten most of the remaining build time.&lt;/p&gt;

&lt;p&gt;TestSprite MCP plugs into Claude Code as an MCP server. It reads the codebase, generates a test plan, and writes runnable test code. I ran it twice: once for a baseline, and again after a round of fixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend tests generated (5/5 passing):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;What it checks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TC001&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /api/dub&lt;/code&gt; with valid file returns &lt;code&gt;{ srt, mp3 }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC002&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /api/dub&lt;/code&gt; with missing params returns 400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC003&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /api/dub&lt;/code&gt; with broken third-party API returns 500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC004&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /api/mux&lt;/code&gt; with valid inputs returns &lt;code&gt;video/mp4&lt;/code&gt; stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC005&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /api/mux&lt;/code&gt; with missing inputs returns 400&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The generated code is more thorough than what you'd write in a hurry. TC001 builds a minimal valid WAV file inline, validates the base64 response actually decodes, and checks the SRT string is non-empty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mp3_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp3_bytes&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;0&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;srt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;json_data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;srt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&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;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Frontend tests generated (12 cases):&lt;/strong&gt; broadcast start and validation, room create/join/leave/rejoin, language and voice change in-session, VOD upload validation, and landing-to-mode navigation flows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the first run caught:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/api/dub&lt;/code&gt; was returning a plain string in some error paths instead of a consistent JSON shape. TC003 found it.&lt;/li&gt;
&lt;li&gt;The room ID field was letting through malformed IDs before hitting the server. TC009 found it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fixed both, reran, all clean. The dashboard keeps a full run history so you can diff before and after. That is the actual useful part: not a single passing run, but a record of what broke, what changed, and whether the fix held.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/your-username/polydub
&lt;span class="nb"&gt;cd &lt;/span&gt;polydub
pnpm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# set DEEPGRAM_API_KEY and LINGO_API_KEY in .env&lt;/span&gt;

pnpm dev     &lt;span class="c"&gt;# terminal 1: Next.js on :3000&lt;/span&gt;
pnpm server  &lt;span class="c"&gt;# terminal 2: WebSocket server on :8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Github: &lt;a href="https://github.com/crypticsaiyan/PolyDub" rel="noopener noreferrer"&gt;https://github.com/crypticsaiyan/PolyDub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>testsprite</category>
      <category>mcp</category>
    </item>
    <item>
      <title>AutoFinance: Your AI Financial Command Center, right inside the terminal!</title>
      <dc:creator>Rahul</dc:creator>
      <pubDate>Mon, 16 Feb 2026 06:22:07 +0000</pubDate>
      <link>https://dev.to/crypticsaiyan/autofinance-your-ai-financial-command-center-right-inside-the-terminal-5anl</link>
      <guid>https://dev.to/crypticsaiyan/autofinance-your-ai-financial-command-center-right-inside-the-terminal-5anl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Leave the Terminal?
&lt;/h2&gt;

&lt;p&gt;As developers, we live in the terminal. So why do we keep switching to 20 different browser tabs just to check market volatility, technical indicators, or portfolio drift?&lt;/p&gt;

&lt;p&gt;I wanted to see if I could build a professional-grade financial research desk that lives exactly where I do. The result is AutoFinance: a heavy-duty orchestration of 13 specialized MCP (Model Context Protocol) servers that turn your command line into a high-speed financial command center.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/sJVKjJSoz7w"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h3&gt;
  
  
  The 13-Server Engine Room
&lt;/h3&gt;

&lt;p&gt;To power these agents, I stood up 13 microservices using the Model Context Protocol. Each server handles a specific domain:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;9001&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Market&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-time data via Yahoo Finance &amp;amp; Binance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9002&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automated trade validation and position sizing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9005&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Technical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Crunches RSI, MACD, and Bollinger Bands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9006&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fundamental&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;P/E ratios, ROE, and growth metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9008&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;News&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local sentiment analysis on market headlines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9012&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Simulation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Monte Carlo scenarios for stress testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📈 Real-Time Market Data&lt;/strong&gt;: Connects directly to &lt;strong&gt;Yahoo Finance&lt;/strong&gt; and &lt;strong&gt;Binance&lt;/strong&gt; (via WebSockets) to stream live prices. No staleness—just raw, real-time data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📡 Multi-Channel Global Notifications&lt;/strong&gt;: Never miss a beat. The system pushes critical alerts to where you actually live:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slack &amp;amp; Discord&lt;/strong&gt;: Get trade confirmations, price spikes, and agent debates in your channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMS &amp;amp; WhatsApp&lt;/strong&gt;: Urgent margin calls or crash alerts sent directly to your phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt;: Daily portfolio summaries and audit reports.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;🛡️ Automated Risk Guardrails&lt;/strong&gt;: The Risk Server enforces strict position limits (max 5%) and stop-losses automatically. It rejects any trade that violates your pre-set safety policy.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;💬 Natural Language Interface&lt;/strong&gt;: Deep &lt;code&gt;copilot&lt;/code&gt; CLI integration lets you "chat" with your portfolio. &lt;em&gt;"Hey, sell half my Bitcoin if RSI &amp;gt; 80."&lt;/em&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;📊 Comprehensive Analysis&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Technical&lt;/strong&gt;: Real-time RSI, MACD, and Bollinger Bands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fundamental&lt;/strong&gt;: P/E ratios, ROE, and growth metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment&lt;/strong&gt;: Scans news headlines to gauge the current market mood.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  How GitHub Copilot CLI Saved My Sanity
&lt;/h2&gt;

&lt;p&gt;It helped me build this project in less than &lt;strong&gt;36 hours!!&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AutoFinance/
├── .github/                       # COPILOT BRAIN 🧠
│   ├── copilot-instructions.md    # The "Bible" (Context &amp;amp; Rules)
│   ├── mcp-config.json            # Local MCP Server Config
│   ├── agents/                    # Specialized Personas
│   │   ├── mcp-server-developer.md
│   │   ├── financial-analyst.md
│   │   ├── cli-dashboard-dev.md
│   │   └── test-engineer.md
│   ├── skills/                    # Reusable Knowledge
│   │   ├── financial-data-validation/
│   │   ├── mcp-server-debugging/
│   │   └── technical-analysis-impl/
│   ├── instructions/              # Path-Specific Context
│   │   ├── mcp-servers.md
│   │   └── tests.md
│   └── hooks/                     # Automation Scripts
│       └── hooks.json             # Pre/Post-execution logic
├── mcp-servers/                   # THE 13 MICROSERVICES
│   ├── market/                    # Yahoo Finance Integration
│   ├── technical/                 # RSI/MACD/Bollinger Logic
│   ├── sentiment/                 # NewsAPI + LLM Analyzer
│   ├── risk/                      # Compliance &amp;amp; Limits Engine
│   └── ... (10 others)
└── cli/                           # The Textual Dashboard UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(I specifically organized &lt;code&gt;.github/&lt;/code&gt; to leverage Copilot's ability to read configuration files. This ensures every suggestion respects the project's architecture.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Building a system with 13 microservices is not easy. Honestly, if I had to manually manage the boilerplate for every server, I’d still be writing imports. &lt;/p&gt;

&lt;p&gt;Here is how Copilot CLI helped me:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The @mcp-server-developer Persona
&lt;/h3&gt;

&lt;p&gt;I defined custom instructions so Copilot knew exactly how I wanted my MCP servers structured. When I needed the &lt;em&gt;Volatility Server&lt;/em&gt;, I didn't write code; I just talked to my terminal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;copilot --agent=mcp-server-developer "Create a volatility server on port 9010 using our standard SSE transport."&lt;/code&gt;&lt;br&gt;
It generated the Pydantic models and error handling that matched the rest of my stack perfectly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Local MCP Integration
&lt;/h3&gt;

&lt;p&gt;I configured Copilot CLI to actually &lt;strong&gt;talk&lt;/strong&gt; to my running AutoFinance servers. By mapping my local tools in &lt;code&gt;~/.copilot/mcp-config.json&lt;/code&gt;, I could ask the CLI real-time questions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Me:&lt;/strong&gt; &lt;code&gt;"What's the current RSI for NVDA?"&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Copilot:&lt;/strong&gt; &lt;code&gt;"The current RSI for NVDA is 72.4 (Overbought). Recommendation: Wait for a pullback."&lt;/code&gt;&lt;br&gt;
It bridged the gap between a coding assistant and a production console.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Living Documentation
&lt;/h3&gt;

&lt;p&gt;Using &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;, I gave Copilot the knowledge of the project—things like port ranges and "no hardcoded API keys" policy. This meant I never had to correct it on the small stuff. It just &lt;em&gt;knew&lt;/em&gt; the architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Landing UI
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx23n3hhoab7lspdawng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx23n3hhoab7lspdawng.png" alt="Landing UI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzarx0sg7p80p646hulh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzarx0sg7p80p646hulh.png" alt="Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Setup in 30 Seconds
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/crypticsaiyan/AutoFinance.git
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; mcp-servers/requirements.txt
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; cli/requirements.txt
./start_sse_servers.sh  &lt;span class="c"&gt;# Fire up the 13-server swarm&lt;/span&gt;
python cli/main.py      &lt;span class="c"&gt;# Launch the Command Center&lt;/span&gt;
&lt;span class="c"&gt;# see the repo for more advanced setup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I really enjoyed and learnt a lot making this project (plus burnt a lot of credits too :P)&lt;/p&gt;

&lt;p&gt;made by: &lt;a href="https://github.com/crypticsaiyan" rel="noopener noreferrer"&gt;crypticsaiyan&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/crypticsaiyan/AutoFinance" rel="noopener noreferrer"&gt;GitHub Repository: crypticsaiyan/AutoFinance&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>cli</category>
      <category>githubcopilot</category>
    </item>
  </channel>
</rss>
