1---2title: "Testing"3sidebar_position: 54---56# Testing78```sh9make check # all of the below that must pass before a push: lint, test, oracle, e2e, then speed10make quick # lint and tests of what changed, for iterating (see Checking a change below)11make lint # gofmt, go vet, staticcheck, cognitive complexity at most 25, Go files at most 500 lines, doclint and doccheck (see Docs below)12make test # engine, file formats and UI unit tests, but for the timed ones13make fuzz # fuzz the formula parser, random edits, and the .012, CSV, .wk1, XLSX and NUON readers14make speed # frames and recalculation against a checked-in baseline, and the tests that time frames (see limits.md)15make oracle # compare formulas and number formats with excelize16make e2e # run the real binary in a terminal emulator (needs Zig and pkg-config)17make screens # rewrite the golden screens and build the review gallery18make stress # benchmarks on synthetic and real data (see limits.md)19make site # build the docs site, failing on broken links (needs Node; see site.md)20```2122## Checking a change2324`make quick` is for iterating. It runs `make lint`'s Go checks and the25unit tests of the packages changed since the branch left main,26committed or not (`PKGS='./internal/sheet ./internal/ui/...'` names27others), the oracle when it changed, and the e2e tests and golden28screens defined in the e2e files that changed: all of e2e when the29harness changed, or with `E2E=1`. It can't tell which e2e tests a change30to `internal/` reaches; `make check` runs them all.3132`make check` is what must pass before a push. Run it once, at the end,33not after every edit while iterating. It runs `make lint`, `make test`,34`make oracle` and `make e2e` at once, each into a log of its own that35it prints whole with the time it ended, then `make speed` on its own:36the speed gate and the tests that time frames against fixed bounds37(`TestSharedEditWithinAFrame`, `TestStreamGridAtFrameSpeed`,38`TestScrollsAtFrameSpeed`), which `make test` leaves out so that no39other tests share the CPU while they time. It takes about half a40minute on an M5 Pro, most of it the e2e tests.4142## Unit tests4344The engine is tested directly: parsing, every function, recalculation,45undo, reference rewriting, file round trips. `FuzzRead` holds the46streaming `.012` reader to encoding/json decoding the whole file: both47refuse a file or both read it into workbooks that save the same bytes.48`TestFixturesOpenAndSaveUnchanged` opens the workbooks every release49saved and saves them again, byte for byte50([File fixtures](releasing.md#file-fixtures)).5152`TestRandomEdits` (`internal/sheet/randedit_test.go`) checks what holds53between features: from each of 3,000 seeds it builds a workbook54of two sheets and a notebook tab, makes up to 40 edits drawn from the55seed (entries, pastes, fills, series, sorts, inserted and deleted rows56and columns, formats, borders, merges, moves, notes, rules, names,57sheets, frozen panes, filters, tables, linked sources and their order,58notebook cells, and outputs sent to sheets, frozen, deleted and sent59rows, with tables standing in for60nushell's), and checks that undoing every step gives back the file and61the values it started with, redoing gives back the end, saving and62reopening gives the same file and values (a recalculation from63scratch), and recalculating everything in place changes no value.64Outputs' rows come from outside the undo history, so the test sends65them again after each edit, undo and reopening, as the UI does. A66failure names the fewest of the seed's edits that still fail. It takes67a few seconds; `-randedit.seeds=20000` runs more, and68`FuzzRandomEdits` (in `make fuzz`) lets the fuzzer choose the edits.6970`TestRandomSharedEdits` (`randedit_shared_test.go`) makes the same71edits as three authors taking turns in one workbook with its history72shared, as a room of `012 serve` holds it, then has them undo their own73steps in turns drawn from the seed, out of order wherever the rule74allows: someone can always undo, an undo redone at once gives back the75file it undid, and once every step is undone the workbook is back where76it started. A fourth author stands for the agent of live mode: its edits are77proposals made on a copy, accepted whole, cell by cell or not at all as78the seed draws, and its accepted steps are undone with the rest.7980The UI is tested by sending81Bubble Tea messages (keys, mouse, paste, window size) to the model and82reading what `View` renders, without a terminal. No unit test waits83on a debounce: a notebook times typing's pauses on a clock the tests84give it (`nbview.View.After`), which hands them the message the pause85would, and a test that wants the pause over delivers it (`endPauses`).86Sessions sharing a87workbook are driven the same way through `ui.Shared`, each room's88changes handed to the others as their programs would get them89(`share_test.go`), and in real Bubble Tea programs typing at once under90the race detector, with the time an edit takes to reach another91session's frame (`sharelive_test.go`).9293The MCP server is tested through the SDK's client over an in-memory94transport. `TestTypedValuesThroughNotebook` writes values of every type95through it, reads them in a notebook cell run by the real `nu` (skipped96without it) and checks the output sent back to a sheet keeps their97formats. `TestViewInBrowser` (`internal/mcp`) draws its98[view](../agents/mcp.md#views-in-the-chat) in Chromium with Playwright99(`internal/mcp/testdata/view.mjs`), once with `window.openai` set as the100OpenAI Apps SDK sets it and once behind a page speaking MCP Apps'101messages, in both themes, and checks the grid, the pointer and formula102bar, scrolling under sticky headers and the frame's height. It needs103Node and the `playwright` package with its browser, and is skipped104without them:105106```sh107npm install --prefix ~/.cache/o12-playwright playwright && npx --prefix ~/.cache/o12-playwright playwright install chromium-headless-shell108PLAYWRIGHT_DIR=~/.cache/o12-playwright go test ./internal/mcp -run TestViewInBrowser109VIEW_SHOTS=/tmp/shots PLAYWRIGHT_DIR=~/.cache/o12-playwright go test ./internal/mcp -run TestViewInBrowser # and screenshots110```111112## End to end, through libghostty113114`e2e/` builds the real `012` binary, runs it on a pseudo-terminal and feeds115its output into [libghostty-vt](https://github.com/ghostty-org/ghostty),116Ghostty's terminal emulator core. Keystrokes and mouse events are encoded117by libghostty itself from the modes the app turned on, so they are the bytes118Ghostty would send. Tests assert on what a user would see: screen text,119cursor position, window title, hyperlinks, which screen is active after120quitting. A fake TypeSafe server answers JEV questions, and the harness121clears `TYPESAFE_API_KEY` so tests never reach the real service. Every122session points `XDG_CONFIG_HOME` into its working directory (with an123optional config file), so its config, theme files and the machine id124that macros' trust uses are the test's own: sessions that share a125directory are one computer. The binary is built with `-tags126fakekeyring`, whose credential store is a file there, so tests never127read your config or touch your keychain. Unit tests do the same with128`ui.Settings` and `keyring.Memory`. The binary is also built with `-tags129crashtest`, where F12 panics in the update, the view or a command, as130the session's environment says (`internal/ui/crashtest.go`), so `e2e/crash_test.go` can check that a crash131restores the terminal and keeps the work; release builds have no such key.132133`e2e/ssh_test.go` runs `012 serve` and reaches it with the system's134`ssh` client inside the same libghostty terminal, so the server path135is tested as a user sees it (skipped when `ssh` isn't installed). `e2e/share_test.go` has two people136reach one `012 serve` that way, each in a terminal of their own, and137share a file; screens of a shared workbook start the others' sessions138first (`screen.peers`). `e2e/live_test.go` runs `012 --listen` and a real139`012 mcp --attach`, spoken to in MCP's JSON-RPC as a host would: the140agent reads, suggests and asks, and the person accepts in the grid;141live mode's screens attach it first (`screen.live`). Live mode's unit142tests (`internal/cowork`) attach the SDK's client over the socket.143144Each test and each golden screen calls `t.Parallel`, so `make e2e` runs145as many sessions at once as Go runs tests (`GOMAXPROCS`): every session146is its own process and pseudo-terminal, in its own temporary directory147with its own config and fake services. A test that shares something148with others, or times keys (`TestStressKeyLatency`), leaves it out. On a149machine this busy a frame can come late, so a test waits for what it150checks (`waitFor`, `eventually`) and never sleeps for it.151152`make e2e` builds libghostty-vt from source with Zig into `.deps/` on first153use. It is its own Go module so cgo never reaches the main binary.154155## Golden screens156157`e2e/screens_test.go` records key UI states as HTML drawn from libghostty's158cell grid (colors as palette variables, so one golden serves every theme),159with light-terminal variants. The states are listed in160`e2e/screenlist_test.go`, with the fixtures they share in161`e2e/screensetup_test.go`. A screen is captured once it holds still162and shows its golden, or once it has held still for five seconds without163showing it: part of a screen can come after it first holds still, such164as nu's highlighting on a busy machine. `make screens` rewrites them and builds165`e2e/testdata/screens/gallery.html` with dark and light reference palettes.166Every visual change is reviewed there before it's committed. It also167draws the docs' stills, the screens `docScreens` lists, into168`docs/media` ([Pictures of 012](site.md#pictures-of-012)); `make e2e`169fails on one not drawn from its golden as it is.170171## The excelize oracle172173`oracle/` translates formulas to Excel syntax, computes them with174[excelize](https://github.com/xuri/excelize), and compares results and175formatted values with 012's. Known differences between Sheets and Excel,176and places where excelize departs from Excel, are listed as skips with a177reason.178179## The Sheets corpus180181012 follows Google Sheets' formula results where people rely on them182([UX](ux.md)), and `internal/fileio/testdata/sheets_corpus.tsv` holds183about 300 formulas that pin it down: dates and times, text, rounding and184floating point, errors and how they propagate, coercion of text,185booleans and blanks, lookups and criteria, array sizes, LET and LAMBDA,186and `TEXT` formats. Each line is a formula, 012's result, Sheets'187result and a note, separated by tabs. Results read `"text"` quoted,188`12.5`, `TRUE`, `#N/A`, or `(blank)`. Formulas read a small fixture on a189sheet named `Data`, and each computes one value: an array is measured190with `ROWS`, `COLUMNS`, `INDEX` or `TEXTJOIN`, so its size is checked191and nothing spills.192193`TestSheetsCorpus` holds 012 to the results recorded for it, and fails194on a row where Sheets' result differs unless its note starts195`on purpose:` and says why; that reason also goes in the docs page for196the feature. Sheets' result reads `unconfirmed` until it is checked in197Sheets:198199```sh200# Add formulas as lines of their own, then record 012's results.201go test ./internal/fileio -run SheetsCorpus -update-corpus202# Write the corpus as a workbook with 012's XLSX exporter.203go test ./internal/fileio -run SheetsCorpus -corpus-xlsx=$HOME/012-sheets-corpus.xlsx204# In Sheets (locale United States): File > Import the workbook, then205# File > Download > CSV of the Corpus sheet. Record Sheets' results:206go test ./internal/fileio -run SheetsCorpus -update-from="$HOME/Downloads/012-sheets-corpus - Corpus.csv"207```208209In the workbook, column C holds each formula, which Sheets recalculates210on import; D and E read its result as text and its kind with formulas211both 012 and Sheets compute, F holds 012's result and G says whether212they agree (for two errors, compare C and F by eye). The exporter saves213formulas Excel has nothing like (`SPLIT`, `REGEXMATCH`, `SORTN`,214`FLATTEN`, `ARRAYFORMULA` inside a formula) as values, so those rows215leave C empty and say so in the note: type the formula from B into C216before downloading, or `-update-from` leaves them unconfirmed. It also217leaves a row unconfirmed where Sheets says `#NAME?` and 012 doesn't: the218import didn't recognize a function, as Sheets doesn't read `FILTER` and219`SORT` written with the `_xlfn._xlws.` prefix Excel gives them. A number220Sheets downloads with a trailing point (`110.` for `=100*1.1`) is read221without it.222223## The XLSX differential test224225`TestXLSXDifferential` (`internal/fileio/xlsx_diff_test.go`) imports a226corpus of workbooks with 012's own XLSX reader and with an importer227built on excelize (`xlsx_excelize_test.go`), and compares228every sheet as saved, the widths, names, sheet shown, rows and notes.229The corpus is workbooks written by the test (hand-written SpreadsheetML,230excelize and 012's exporter), the stress datasets' XLSX files when231fetched (`STRESS_DIR`), and any directories in `XLSX_CORPUS`, such as232excelize's own `test/` files in the module cache. Where excelize reads a233file wrongly (formulas spread across merged cells, spaces dropped from234shared formulas) the comparison leaves the difference out, with the235reason next to it.236237## Live checks238239`JEV_LIVE_TEST=1 go test ./internal/jev -run TestLive` asks the real JEV240service one question of each kind, using `TYPESAFE_API_KEY`.241242## Demo recordings243244`demos/` holds [VHS](https://github.com/charmbracelet/vhs) tapes for the245recordings and stills the README and the docs show. `make demos` renders246all of them (or `make demos DEMOS=jev` for one) into `demos/out/`: full247GIFs, PNG stills of key moments in `demos/out/stills/`, and smaller GIFs248in `demos/out/media/`. It needs vhs 0.12+, ttyd and ffmpeg, and installs249the pictures' font if it's missing; each tape's color scheme follows the250pages that show it ([Font and colors](site.md#font-and-colors)). The JEV tape251talks to `demos/fakejev`, a local stand-in the target starts on252127.0.0.1, never the real service. On macOS, `demos/lib/ttyd` wraps ttyd253so Alt+letter reaches the app. VHS has no mouse commands, so the tapes254use the keyboard, and they reach commands through the palette (Ctrl+K)255rather than counting menu items, so a new menu item doesn't break them.256Tapes that change settings set them for the run (`O12_KEYMAP=vim`)257rather than writing the config file the other tapes share. VHS's ttyd258has sixel on, so 012 draws charts as sixel images and the recordings259show them ([Charts](../sheets/charts.md)).260The nushell tapes need `nu`: `demos/lib/nu.tape` starts a session261without your config files, with a fixed prompt and 012 on the PATH, and262their data is in `demos/data`, so each recording comes out the same.263264A change to what a tape shows means rendering it again, reading its265stills, and copying what the docs show into `docs/media/`: GIFs from266`demos/out/media/`, stills from `demos/out/stills/`. Only files a doc267shows go there, and GIFs stay under 1 MB.268269## Docs270271The docs are a tree of folders by concept under `docs/`, Markdown that272reads on GitHub and builds with Docusaurus as it is:273274- each folder has a `README.md` that links its pages and subfolders (the275 folder's page in Docusaurus, and what GitHub shows for the folder), and276 a `_category_.json` with its sidebar `label`, `position` and a link to277 that README;278- each page starts with front matter: a `title` and a `sidebar_position`279 unique in its folder;280- links are relative, to `.md` files, so they work in both; images live281 in `docs/media/`.282283A new page goes in the folder a reader would look in, with front matter,284and a line in the folder's README. `make site` builds them into the285[docs site](site.md) and fails on any link or anchor it can't resolve.286287`make lint` runs `scripts/doclint`, which flags wording that narrates288history (see [CLAUDE.md](../../CLAUDE.md)) and code blocks that draw289012's screen ([Pictures of 012](site.md#pictures-of-012)), and `scripts/doccheck`, which290checks the tree above, that relative links and anchors resolve, that291every `docs/...md` path Go code names exists, that every file in292`docs/media` is shown by a doc and made by a tape or drawn from a golden293screen, that every tape and still is shown by a doc, and that a page the site has published294isn't gone without a redirect ([The docs site](site.md#old-addresses-keep-working)).295Unit tests check what the docs say about296the code: menu paths lead to menu items, command ids and keys exist and297every bound key is in [Keys and mouse](../reference/keys.md)298(`internal/ui/docs_test.go`), settings and variables are options299(`internal/config/docs_test.go`), and [the .012 format](../files/format.md)300names every field (`internal/sheet/file_doc_test.go`).301[Functions](../reference/functions.md) and the reference in302[Configuration](../reference/config.md) are generated, and their tests303fail when they're stale.304305## No broken windows306307`make check` must pass before a push ([Checking a change](#checking-a-change)),308and warnings are fixed or turned off with a written reason rather than309left standing. The rules are in310[CLAUDE.md](../../CLAUDE.md).