<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ForkSpace]]></title><description><![CDATA[ForkSpace]]></description><link>https://forkspace.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>ForkSpace</title><link>https://forkspace.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 22 Sep 2026 08:55:41 GMT</lastBuildDate><atom:link href="https://forkspace.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[LLMs are terrible at generating correct test cases. Here's the architecture that actually works.]]></title><description><![CDATA[I want to start with the mistake, because that's where the real lesson is.
When I first added hidden test generation to ForkSpace — my real-time collaborative DSA practice platform — the obvious appro]]></description><link>https://forkspace.hashnode.dev/llms-are-terrible-at-generating-correct-test-cases-here-s-the-architecture-that-actually-works</link><guid isPermaLink="true">https://forkspace.hashnode.dev/llms-are-terrible-at-generating-correct-test-cases-here-s-the-architecture-that-actually-works</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[System Design]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Competitive programming]]></category><dc:creator><![CDATA[Sundram Rai]]></dc:creator><pubDate>Thu, 30 Apr 2026 16:32:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69e5fea41faed8cd147fceb0/ce540638-a947-4514-8ab5-5dd7a12da069.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I want to start with the mistake, because that's where the real lesson is.</p>
<p>When I first added hidden test generation to <a href="https://fork-space.vercel.app/">ForkSpace</a> — my real-time collaborative DSA practice platform — the obvious approach seemed straightforward: give the LLM the problem statement, ask it to generate some edge case inputs, and ask it to also tell me the expected output for each one. Then run the user's code, compare outputs, done.</p>
<p>It failed. Badly.</p>
<p>For a simple array reversal problem, it worked fine. But the moment I tested it on a graph problem with specific connectivity constraints, the LLM confidently generated an input — and just as confidently invented a wrong expected output. My system was now telling users their <em>correct solutions were wrong</em>. That's not a testing assistant. That's a gaslighting machine.</p>
<p>I had to tear the whole thing down and rethink it from scratch.</p>
<hr />
<h2>Why the obvious approach breaks</h2>
<p>Here's the thing about LLMs and competitive programming: they're excellent at understanding problem <em>structure</em>. They can look at a problem statement and immediately reason about what kinds of inputs would stress a solution — boundary values, empty cases, overflow triggers, worst-case graphs. That's genuine reasoning ability.</p>
<p>But expected outputs? For non-trivial problems, LLMs hallucinate. Not always. Not obviously. Just enough to be dangerous.</p>
<p>A wrong expected output in a test suite is worse than no test at all. At least with no test, you're not actively misleading the user. But when your "automated judge" says their O(n log n) solution failed on a specific graph input — and the expected output your LLM invented was simply wrong — you've just destroyed trust in your entire system.</p>
<p>The root cause is this: <strong>LLMs are planners, not compilers.</strong> Asking an LLM to produce a correct expected output for a complex input is asking it to mentally simulate code execution. Sometimes it can. Often it can't. And it has no reliable way to tell you which situation you're in.</p>
<p>So the architecture needed to reflect that reality.</p>
<hr />
<h2>The split that fixed everything</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69e5fea41faed8cd147fceb0/bc65ab22-f7c2-47c2-9dca-707c0a14f7ee.png" alt="" style="display:block;margin:0 auto" />

<p>The system I ended up building has one core principle:</p>
<blockquote>
<p><strong>The LLM plans. Code generates. Piston executes. Nobody asks the LLM to be a judge.</strong></p>
</blockquote>
<p>Here's what that looks like in practice.</p>
<h3>Step 1: LLM does only one job — planning</h3>
<p>The LLM receives the problem statement, constraints, and sample I/O. It returns a JSON array of test <em>plans</em>. Not test inputs. Plans.</p>
<p>Here's the actual prompt:</p>
<pre><code class="language-plaintext">You are a test planning assistant for competitive programming.
Return ONLY valid JSON array. No markdown. No prose.
Generate 4 concise hidden test plans.
Do NOT provide expected outputs.
Do NOT provide raw stdin strings.

Schema:
[
  {
    "category": "boundary|stress|robustness|randomized",
    "description": "short description",
    "bugClass": "off-by-one|overflow|empty case|wrong loop bounds|unknown",
    "inputSpec": { "type": "array|string|graph", ... }
  }
]

Problem statement:
${problemStatement}

Constraints:
${constraints}

Sample I/O:
${sampleIO}
</code></pre>
<p>Two lines in that prompt are doing most of the architectural work:</p>
<p><code>Do NOT provide expected outputs.</code> This is the entire philosophical stance of the system in one instruction. You're explicitly telling the LLM what it's unreliable at and removing it from that responsibility entirely.</p>
<p><code>Do NOT provide raw stdin strings.</code> This keeps the LLM in the abstract layer. It describes <em>what</em> to generate — the shape, the category, the intent. Your code decides <em>how</em> to format it. Same plan always produces consistent, valid input. LLM-generated stdin has unpredictable whitespace, newline handling, and formatting variance that breaks execution in ways that are silent and maddening to debug.</p>
<p>The simplest mental model: <strong>the LLM is a senior engineer writing a test plan doc. It thinks about categories and edge cases. It never touches a compiler.</strong></p>
<h3>Step 2: Deterministic code handles generation</h3>
<p>The plans come out of the LLM looking like this:</p>
<pre><code class="language-json">{
  "category": "boundary",
  "description": "single element array",
  "bugClass": "off-by-one",
  "inputSpec": { "type": "array", "size": 1, "valueRange": [1, 1000] }
}
</code></pre>
<p><code>inputGenerator.js</code> takes that inputSpec and constructs the actual stdin string. Right now it handles arrays, strings, and graphs. The point is: generation is fully deterministic and constrained. No LLM variance, no formatting surprises. The same plan produces the same valid input every time.</p>
<p>This is the "LLM for planning, code for generation" pattern. It's much safer than "LLM generates everything" because it plays to each component's actual strengths.</p>
<h3>Step 3: Piston runs the code, classifies the result</h3>
<p>The user's code plus the generated stdin goes to <a href="https://github.com/engineer-man/piston">Piston</a> for execution. Piston is an isolated code execution engine — it runs C++, Python, and JavaScript cleanly.</p>
<p>After execution, <code>classifyOutput</code> labels each result:</p>
<ul>
<li><p><strong>pass / fail</strong> — only for <em>verified</em> tests where a trusted expected output exists</p>
</li>
<li><p><strong>timeout</strong> — execution exceeded the time limit</p>
</li>
<li><p><strong>crash</strong> — runtime error, segfault, bad memory access</p>
</li>
<li><p><strong>stress-only</strong> — no expected output exists, so we can't pass/fail, but the execution behavior is still a signal</p>
</li>
</ul>
<p>That last category is the key design decision.</p>
<hr />
<h2>Verified tests vs stress tests — why the split matters</h2>
<p>The system runs two kinds of tests and it's important to understand why they're separate.</p>
<p><strong>Verified tests</strong> are built from existing room sample cases plus any curated additions. These have trusted expected outputs. Pass/fail is strict and reliable.</p>
<p><strong>Stress tests</strong> come from the AI-generated plans. They have no ground truth output. We never invented one. Instead, we run the user's code and watch what happens — does it crash? Does it timeout? Does it produce suspicious output? These are signals, not verdicts.</p>
<p>This is intellectually honest in a way most automated testing tools aren't. The system won't tell you your solution is wrong unless it actually knows the right answer. For stress tests, it tells you: "your code crashed on this input" or "your code timed out on this graph" — which is genuinely useful information without requiring a ground truth.</p>
<p>There's also a promotion flow: if a stress test produces interesting behavior, the user can adopt the generated output as a new sample case for debugging in the room. The "correct" answer emerges from human judgment, not LLM invention.</p>
<hr />
<h2>The part I didn't expect to matter: bugClass</h2>
<p>When I designed the prompt schema, I added <code>bugClass</code> as a metadata field mostly for display purposes. It turned out to be the most valuable field in the whole system.</p>
<p>Here's why: hidden test outcomes feed directly into session intelligence reports. After a practice session, the system summarizes what happened — strengths, gaps, patterns. And <code>bugClass</code> is what makes those patterns specific.</p>
<p>Instead of "you failed 3 hidden tests," the report can say: "you failed 3 tests classified as <code>off-by-one</code> — check your loop termination conditions." That's the difference between a score and a learning signal.</p>
<p>The LLM isn't just planning test shapes. It's labelling the <em>intent</em> of each test — what class of bug it's designed to expose. That label flows downstream through execution and into the user's learning outcome. I didn't plan that pipeline upfront. It emerged from having the right field in the schema from the start.</p>
<hr />
<h2>What the full flow looks like</h2>
<pre><code class="language-plaintext">Workspace UI
  → /api/hidden-tests/generate
      → room problem statement + constraints + samples
      → LLM planner (Gemini, fallback to safe defaults)
      → structured input generator
      → save test plans

Workspace UI
  → /api/hidden-tests/run
      → fetch saved tests
      → Piston execution
      → classify output (pass/fail/timeout/crash/stress-only)
      → save results

Hidden test results
  → session intelligence report
      → bugClass pattern aggregation
      → learning signal surface
</code></pre>
<p>One more reliability detail worth mentioning: if MongoDB is unavailable, the router falls back to an in-memory room map. If Gemini is unavailable or hits quota, the planner falls back to safe default plans. The feature degrades gracefully instead of breaking. In a tool people use during timed practice sessions, silent failures are unacceptable.</p>
<hr />
<h2>What this system is not</h2>
<p>I want to be honest about the limitations because I think intellectual honesty is what separates useful technical writing from marketing.</p>
<p>This is not a full competitive programming judge with official expected answers for all hidden tests. It won't give you the same pass/fail guarantee as Codeforces or LeetCode's hidden judge. It can't — it doesn't have a reference solution, and it's not going to pretend an LLM-invented answer is ground truth.</p>
<p>What it's actually good at: surfacing crashes, timeouts, edge case behavior, and suspicious output patterns. For collaborative practice and mock interviews — which is what ForkSpace is built for — that's exactly the right scope.</p>
<p>The input generator currently handles arrays, strings, and graphs. More structured types would meaningfully expand coverage. That's the most obvious next build.</p>
<hr />
<h2>What I'd build next</h2>
<p>The <code>bugClass</code> field opens up a feedback loop I haven't closed yet.</p>
<p>Right now, the LLM plans tests based on the problem statement. It doesn't know anything about what kinds of bugs users on this platform actually hit most frequently. But that data exists in the session reports.</p>
<p>The natural next step: feed aggregated failure patterns back into the planning prompt. If users consistently hit <code>overflow</code> bugs on problems with large integer constraints, weight the planner toward generating more overflow-class tests for similar problems. The system gets smarter about what to test based on what people actually get wrong.</p>
<p>That's a loop from session data → planner prompt → test quality → session data. I haven't built it yet. But naming it felt important because it's where this kind of system eventually has to go to be genuinely useful.</p>
<hr />
<h2>The one-line version</h2>
<p>If I had to reduce the entire architecture to a single principle:</p>
<p><strong>Ask the LLM what to test. Ask your code to build it. Ask a real executor to run it. Never ask the LLM to grade it.</strong></p>
<p>That split — planning vs generation vs execution vs judgment — is what makes the system reliable. Each component does what it's actually good at.</p>
<hr />
<p>If you want to poke around the implementation, the main files are <code>testPlanner.js</code>, <code>inputGenerator.js</code>, <code>pistonRunner.js</code>, and <code>hiddenTestRoutes.js</code> in the <a href="https://github.com/Sundramrai3691/ForkSpace">ForkSpace repo</a>.</p>
<p>If you've built something similar or hit different failure modes with LLM-generated tests, I'd genuinely like to know what you found. The failure modes are where the interesting design decisions live.</p>
<hr />
<p><em>ForkSpace is a real-time collaborative DSA practice platform for peer sessions, mock interviews, and mentoring. If you're preparing for placements and want to practice with a partner,</em> <a href="https://fork-space.vercel.app/"><em>give it a try</em></a><em>.</em></p>
]]></content:encoded></item></channel></rss>