Programming
-
From SUnit to Vitest: A Brief History of JavaScript Testing
I care a lot about testing. I don’t know if that’s obvious yet, but hopefully it’s obvious. I wanted to trace the lineage of the testing tools we use today, because I think understanding where they came from helps you appreciate why things work the way they do.
Where It All Started
Automated testing as we know it really started with SUnit for the Smalltalk language. Kent Beck created it back in 1989, and it established the patterns that every test framework still borrows from today.
In 1997, Kent Beck and Erich Gamma ported those ideas to Java and created JUnit. JUnit was, or is, incredibly influential to pretty much every unit testing framework you’ve ever used. The test runner, assertions, setup and teardown, all of that traces back to JUnit.
But I’m going to focus on the JavaScript side of things here.
Jest: The Facebook Era
Jest was originally created at Facebook in 2011 as part of a major platform rewrite. It became the dominant testing framework for React and Node.js codebases, and in 2022, Facebook released it to the OpenJS Foundation.
Jest works well, but it carries some baggage. It requires a transpilation pipeline, something that was common a decade ago but feels burdensome now. If you want to use ESM modules, there’s an extra step involved. It’s adds friction.
So what else is there?
Vitest: The Modern Alternative
Vitest is a modern alternative, built on top of Vite. It supports ESM modules and TypeScript out of the box; so no transpilation step needed. And because Vite has HMR (hot module replacement), the watch mode for rerunning tests is very fast.
Vitest was initially created by Anthony Fu and the company behind Vue.js. The initial commit was in December 2021, so it’s a relatively recent project. They’ve made incredible progress since then.
Vitest uses Vite under the hood. So lets look at that briefly.
Why Vite Is So Fast
Vite’s speed comes from esbuild, which is written in Go. It compiles directly to native machine code, so it bypasses the JavaScript engine’s overhead entirely. It can transform TypeScript significantly faster because it doesn’t need to go through the JS engine. And because it’s Go, it’s multithreaded.
But things are changing. In Vite 8, the bundler is moving from esbuild to Rolldown. This is new tool written in Rust that combines the best of esbuild and Rollup.
Why? Currently, Vite uses esbuild during development but switches to Rollup for production builds. Two different tools for two different use cases. Rolldown unifies both into a single tool that handles dev and production.
What Did We Learn?
Hopefully something! How about a mini review to nail it home:
- 1989: SUnit (Smalltalk) — Kent Beck starts it all
- 1997: JUnit (Java) — the template everything else follows
- 2011: Jest — Facebook’s testing framework, now under OpenJS Foundation
- 2021: Vitest — modern, fast, ESM-native testing built on Vite
- Coming soon: Rolldown replaces esbuild + Rollup in Vite 8
That’s all for now!
-
Garbage Collection: How Python, JavaScript, and Go Clean Up After Themselves
It’s Garbage day for me IRL and I wanted to learn more about garbage collection in programming. So guess what? Now you get to learn more about it too.
We’re going to focus on three languages I work with mostly, Python, JavaScript, and Go. We will skip the rest for now.
Python: Reference Counting
Python’s approach is the most straightforward of the three. Every object keeps track of how many things are pointing to it. When that count drops to zero, the object gets cleaned up immediately. Simple.
There’s a catch, though. If two objects reference each other but nothing else references either of them, the count never hits zero. That’s a reference cycle, and Python handles it with a secondary cycle detector that periodically scans for these orphaned clusters. But for the vast majority of objects, reference counting does the job without any fancy algorithms.
JavaScript (V8): Generational Garbage Collection
Most JavaScript you encounter is running on V8… so Chrome based browsers, Node and Deno. V8 uses a generational strategy based on a simple observation: most objects die young.
V8 splits memory into two main areas:
- The Nursery (Young Generation): New objects land here. This space is split into two halves and uses a scavenger algorithm. It’s fast because it only deals with short-lived variables — and most variables are short-lived.
- Old Space (Old Generation): No, not the deodorant company. Objects that survive a couple of scavenger rounds get promoted here. Old space uses a mark-and-sweep algorithm, which is slower but handles long-lived objects more efficiently.
First it asks, “How long has this object been around?” New stuff gets the quick treatment, and anything that sticks around gets put out to the farm, to be delt where time is less of a premium. It’s a smart tradeoff between speed and memory efficiency.
Go: Tricolor Mark-and-Sweep
Go’s garbage collector also uses mark-and-sweep, but with a twist called tricolor marking. Here’s how it works:
- White objects: These might be garbage but haven’t been checked yet. Everything starts as white.
- Gray objects: The collector has reached these, but it hasn’t scanned their children (the things they reference) yet.
- Black objects: These are confirmed alive — the collector has scanned them and all their references.
The collector starts from known root objects, marks them gray, then works through the gray set — scanning each object’s references and marking them gray too, while the scanned object itself turns black. When there are no more gray objects, anything still white is unreachable and gets cleaned up.
Go’s approach is notable because it runs concurrently. This helps with latency while the GC is running.
Garbage, Collected
Each approach reflects the language’s priorities:
- Python optimizes for simplicity and predictability. Objects get cleaned up correctly when they’re no longer needed
- JavaScript optimizes for speed in interactive applications. Quick cleanup for short-lived objects, thorough cleanup for the rest
- Go optimizes for low latency. Concurrent collection is great for server side processes
References
- Design of CPython’s Garbage Collector — Python Developer’s Guide deep dive into reference counting and cycle detection
- Orinoco: Young Generation Garbage Collection — V8’s parallel scavenger and generational GC design
- A Guide to the Go Garbage Collector — Official Go documentation on the concurrent tricolor collector
-
How Smart People Are Using Claude Code Skills to Automate Anything
🚀 Build agentic systems that run your business: skool.com/scrapes Don’t miss the next build - www.youtube.com/@simonscr…
-
Everything Is Eventually a Database Problem
I think there’s a saying that goes something like “code is ephemeral, but data is forever.” That’s never been more true than right now. Code is easier than ever to create, anyone can spin up a working app with an AI agent and minimal experience. But your data structure? That’s the thing that sticks around and haunts you.
Data modeling is one of those topics that doesn’t get enough attention, especially given how critical it is. You need to understand how your data is stored, how to structure it, and what tradeoffs you’re making in how you access it. Get it right early, and your code stays elegant and straightforward. Get it wrong, and your codebase becomes a forever series of workarounds…
Microservices Won’t Save You
For teams moving from a monolith to microservices, if the data stays tightly coupled, you don’t really have microservices; you have a distributed monolith with extra network hops.
Yes, data can be coupled just like code can be coupled. If all your different services are still hitting the same database with the same schema, you have a problem. You need separate data structures for your services, not a monolithic architecture hiding behind a microservices facade.
The Caching Trap
So what happens when you have a lot of data and your queries get slow? You’ve done all the easy stuff; optimized queries, added indexes, followed best practices. But things are still slow.
Every senior engineer’s first instinct is the same: “Let’s add Redis in front of it.” Or “more read replicas.” And sure, that works, but you have just added complexity and now you have to deal with cache invalidation.
What happens when you have stale data? How do you recache current data, and when does that happen?
Are you caching on the browser side too? Understanding where data can be cached and how to invalidate it is another genuinely difficult problem to solve. You’re just trading one set of problems for a different set of problems.
You Can’t Predict Every Future Question
If you’re selling things on the internet, chances are, you will care about event sourcing at some point. A lot of interesting business problems don’t care about the current state of a user, they care about the intent and history. So how you store intent and history is probably different from your ACID-compliant Postgres table that you’ve worked hard to normalize.
You can get your data structure perfect for displaying products and processing sales, then run into a completely new set of requirements that changes everything about how your data needs to be structured.
It’s genuinely hard to foresee all the potential questions you’ll need to answer in the future.
Why This Matters Now
Everything you do on a computer stores data somewhere, it’s just a matter of persistence.
Which is why; everything software-related is eventually a database problem.
Data modeling isn’t glamorous, but getting it right is the difference between a system that scales gracefully and one that fights you every step of the way.
-
Your Context Window Is a Budget — Here's How to Stop Blowing It
If you’re using agentic coding tools like Claude Code, there’s one thing you should know by now: your context window is a budget, and everything you do spends it.
I’ve been thinking about how to manage the budget. As we are learning how to use sub-agents, MCP servers, and all these powerful capabilities we haven’t been thinking enough about the cost of using them. Certainly the dollars and cents matters too if you are using API access, but the raw token budget you burn through in a single session impacts us all regardless. Once it’s gone, compaction kicks in, and it’s kind of a crapshoot on whether it knows how to pick up where we left off on the new session.
Before we talk about what you can do about it, let’s talk about where your tokens go, or primarily are used.
Why Sub-Agents Are Worth It (But Not Free)
Sub-agents are one of the best things to have in agentic coding. The whole idea is that work happens in a separate context window, leaving your primary session clean for orchestration and planning. You stay focused on what needs to change while the sub-agent figures out how.
Sub-agents still burn through your session limits faster than you might expect. There are actually two limits at play here:
- the context window of your main discussion
- the session-level caps on how many exchanges you can have in a given time period.
Sub-agents hit both. They’re still absolutely worth using and working without them isn’t an option, but you need to be aware of the cost.
The MCP Server Problem
MCP servers are another area where things get interesting. They’re genuinely useful for giving agentic tools quick access to external services and data. But if you’ve loaded up a dozen or two of them? You’re paying a tax at the start of every session just to load their metadata and tool definitions. That’s tokens spent before you’ve even asked your first question.
My suspicion, and I haven’t formally benchmarked this, is that we’re headed toward a world where you swap between groups of MCP servers depending on the task at hand. You load the file system tools when you’re coding, the database tools when you’re migrating, and the deployment tools when you’re shipping. Not all of them, all the time.
There’s likley more subtle problems too. When you have overlapping MCP servers that can accomplish similar things, the agent could get confused about which tool to call. It might head down the wrong path, try something that doesn’t work, backtrack, and try something else. Every one of those steps is spending your token budget on nothing productive.
The Usual Suspects
Beyond sub-agents and MCP servers, there are the classic context window killers:
- Web searches that pull back pages of irrelevant results
- Log dumps that flood your context with thousands of lines
- Raw command output that’s 95% noise
- Large file reads when you only needed a few lines
The pattern is the same every time: you need a small slice of data, but the whole thing gets loaded into your context window. You’re paying full price for information you’ll never use.
And here’s the frustrating part — you don’t know what the relevant data is until after you’ve loaded it. It’s a classic catch-22.
Enter Context Mode
Somebody (Mert Köseoğlu - mksglu) built a really clever solution to this problem. It’s available as a Claude Code plugin called context-mode. The core idea is simple: keep raw data out of your context window.
Instead of dumping command output, file contents, or web responses directly into your conversation, context-mode runs everything in a sandbox. Only a printed summary enters your actual context. The raw data gets indexed into a SQLite database with full-text search (FTS5), so you can query it later without reloading it.
It gives Claude a handful of new tools that replace the usual chaining of bash and read calls:
- ctx_execute — Run code in a sandbox. Only your summary enters context.
- ctx_execute_file — Read and process a file without loading the whole thing.
- ctx_fetch_and_index — Fetch a URL and index it for searching, instead of pulling everything into context with WebFetch.
- ctx_search — Search previously indexed content without rerunning commands.
- ctx_batch_execute — Run multiple commands and search them all in one call.
There are also slash commands to check how much context you’ve saved in a session, run diagnostics, and update the plugin.
The approach is smart. All the data lives in a SQLite FTS5 database that you can index and search, surfacing only the relevant pieces when you need them. If you’ve worked with full-text search in libSQL or Turso, you’ll appreciate how well this maps to the problem. It’s the right tool for the job.
The benchmarks are impressive. The author reports overall context savings of around 96%. When you think about how much raw output typically gets dumped into a session, it makes sense. Most of that data was never being used anyway.
What This Means for Your Workflow
I think the broader lesson here is that context management is becoming a first-class concern for anyone doing serious work with agentic tools. It’s not just about having the most powerful model, it’s about using your token budget wisely so you can sustain longer, more complex sessions without hitting the wall.
A few practical takeaways:
- Be intentional about MCP servers. Load what you need, not everything you have.
- Use sub-agents for heavy lifting, but recognize they cost session tokens.
- Avoid dumping raw output into your main context whenever possible.
- Tools like context-mode can dramatically extend how much real work you get done per session.
We’re still early in figuring out the best practices for working with these tools. But managing your context window? That’s one of the things that separates productive sessions from frustrating ones.
Hopefully something here saves you some tokens.
-
I got tired of plaintext .env files, so I built LSM.
lsm execwill inject secrets at runtime so they never touch the filesystem. Doppler’s idea, minus the monthly bill.How are you managing local secrets?
-
I switched to mise for version management a month ago. No regrets. No more
brew upgradebreaking Python. Built-in task runner replaced some of projects that were using Makefiles.Still juggling nvm + pyenv + rbenv?
-
I wrote about why you should stop using pip. Poetry or uv. Pick one. pip has no lockfile, no dependency resolution worth trusting, and no isolation by default.
Have you moved to uv yet? Still happy with poetry? How’s it going?
-
I wrote about when to reach for Python over Bash. Most veterans put the cutoff at 50 lines. If you’re writing nested if statements in Bash, you’ve already gone too far.
Where does Go fit in though? Overkill for scripts?
-
With AI agents, the cost of writing tests is approaching zero. Five words: “write tests on new code.” The excuses are running out.
Are you verifying the tests your AI writes, or just trusting the green checkmarks?