-
-
Notifications
You must be signed in to change notification settings - Fork 1
Testing Scenarios
Coco ships with a scenario library for spinning up real git repos in deterministic, named states — useful for trying out the TUI against tricky shapes (merge conflicts, diverged branches, mid-bisect, multi-remote forks) without manually building those states by hand.
Scenarios live in @gfargo/git-scenarios, a small package extracted from coco's internal test utilities. Coco depends on it and re-exposes the CLI as npm run scenario.
Two ways to use scenarios:
-
Manual testing — materialize a scenario in a tmp dir and launch
coco ui(or any other git tool) against it. Tightest dev loop for trying TUI changes. -
Integration tests — use
spinUpScenario('feature-pr-ready')in your jest tests to get a fully-builtTempGitRepoinstead of writing inlinegit init+ commit-loop boilerplate.
Both paths share the same library, so scenarios you try manually are the exact same shapes the test suite asserts against.
# List every scenario, grouped by kind:
npm run scenario list
# Describe a single scenario:
npm run scenario describe feature-pr-ready
# Materialize a scenario in /tmp:
npm run scenario create feature-pr-ready
# Materialize and immediately launch coco ui against it:
npm run scenario create feature-pr-ready -- --run-ui
# Materialize at a custom path:
npm run scenario create feature-pr-ready -- --path ~/sandbox/widget
# Auto-clean the tmp dir when the launched tool exits:
npm run scenario create feature-pr-ready -- --run-ui --ephemeralThe -- separator is required because npm run scenario already accepts subcommands; flags after -- are passed through to the underlying git-scenarios binary.
Current set: 19 scenarios across 6 kinds. The live list is npm run scenario list — the table below is a quick reference.
| Name | What you get |
|---|---|
feature-pr-ready |
feat/widget-v2 4 commits ahead of main, clean worktree. The "ready to open a PR" baseline — drives the C create-pr flow and the L changelog flow. |
feature-branch-one-commit |
main + feat/x (1 commit ahead with src/feature.ts). Minimal branch-vs-base shape. |
multi-commit-branch |
feat/dashboard with 8 varied commits. Baseline for history navigation, filter, yank, etc. |
two-commit-feature |
Baseline + a feat: commit on main, clean worktree. Smoke-test fixture. |
These are the prereq fixtures for testing pull/push UI, ahead/behind indicators, and the branch-tip chip rendering.
| Name | What you get |
|---|---|
branch-tracking-upstream |
main tracks origin/main, both at the same commit. Baseline "synced" state. |
branch-ahead-of-upstream |
main is 3 commits ahead of origin/main. Classic "unpushed changes" state. |
branch-behind-upstream |
main is 3 commits behind origin/main (built with withRemoteTracking). Fast-forwardable. |
branch-diverged |
main is 2 ahead AND 2 behind origin/main. The "pull --rebase" case. |
multi-remote-with-tracking |
Fork workflow: origin + upstream remotes, main tracks upstream/main, feat/fork-work tracks origin/feat/fork-work. |
chip-rendering-showcase |
Six commits each carrying a different branch-tip-chip kind (HEAD, plain local, slashy local, two distinct remote refs, tag). Single screen of history exercises every chip code path. |
| Name | What you get |
|---|---|
detached-head |
HEAD detached at main~2, main still at its original tip. |
signed-commits-required |
commit.gpgsign=true + user.signingkey set. Commits in the scenario itself are unsigned (CI lacks GPG); tools can read the config to decide whether to nudge the user. |
| Name | What you get |
|---|---|
single-staged-file |
Baseline + 1 staged README. Minimum "ready to commit" shape. |
dirty-many-files |
12 staged + 6 unstaged + 3 untracked files across src/, tests/, docs/. The fixture for the future split flow. |
| Name | What you get |
|---|---|
mid-bisect |
20 commits + active git bisect, HEAD at midpoint. Drives the bisect view. |
mid-merge-conflict |
In-progress merge with 1 unresolved conflict on src/widget.ts. Drives the conflicts view. |
| Name | What you get |
|---|---|
rich-history-graph |
20+ commits across 6 date buckets, 2 --no-ff merges, 1 live unmerged feat/wip. Exercises compact + full-graph rendering, bucket dividers, type colouring, lane topology. |
| Name | What you get |
|---|---|
stashed-changes |
Clean main + 3 stashes (LIFO ordered, each touching a distinct file). Drives the stash view. |
submodule-with-history |
Parent with 4 commits + vendor/lib submodule (clean pin, 4 commits, branch = main). Drives recursive submodule navigation. |
# Use the showcase to see every branch-tip-chip variant at once:
npm run scenario create chip-rendering-showcase -- --run-uiLook at the history view — you should see:
- Row 0 (HEAD): green chip for
main - Row 1: yellow chip for
origin/main - Row 2: blue chip for
feat/widgets(slash, but still local!) - Row 3: yellow chip for
upstream/main - Row 4: blue chip for
develop - Row 5: no chip, but
[tag: v0.1.0]in trailing decoration
If any of those colours is wrong, that's a regression.
npm run scenario create mid-merge-conflict -- --run-uiLands you in a repo with a real in-progress merge and a real conflict on disk — no mocking needed.
npm run scenario create branch-diverged -- --run-uigit status reports "2 ahead, 2 behind." Test that the UI surfaces that and that pull/push affordances do the right thing.
npm run scenario create feature-pr-ready -- --run-ui --ephemeralTmp dir gets removed when coco ui exits.
npm run scenario create feature-pr-ready -- --remote git@github.com:org/repo.git --run-uiAdds origin pointing at the given URL. Useful for testing gh-aware flows (PR creation, issue triage). Use a fake URL if you don't want any risk of accidental push.
The scenarios listed above cover common cases. If you need something different, two paths:
The @gfargo/git-scenarios package exposes the atom layer that scenarios are built from. Compose what you need inline:
import {
createTempGitRepo,
chain,
addCommit,
addRemote,
setUpstream,
withRemoteTracking,
} from '@gfargo/git-scenarios'
const repo = await createTempGitRepo()
await chain(
addCommit({ message: 'init', files: { 'README.md': '# repo' } }),
addRemote('origin', '/fake/url'),
withRemoteTracking('origin', 'main', chain(
addCommit({ message: 'upstream B', files: { 'b.ts': 'b' } }),
addCommit({ message: 'upstream C', files: { 'c.ts': 'c' } }),
)),
setUpstream('main', 'origin'),
)(repo)
// repo is now 2 commits behind origin/main, ready for assertionsSee git-scenarios cookbook for many more recipes.
If the shape is reusable, contribute it to @gfargo/git-scenarios:
- Add
src/scenarios/your-scenario.tsusingdefineScenario(...)(see existing scenarios for the pattern — every one is a single self-contained file). - Add a
.test.tsasserting your scenario's contracts. - Wire it into
src/scenarios/index.ts(allScenariosarray + the re-export). - Run
npm testto verify; submit a PR togfargo/git-scenarios.
A scenario is roughly 30–60 lines including JSDoc, and defineScenario validates the shape at module-load time so typos surface immediately.
The integration test at src/commands/commands.integration.test.ts is the worked example for replacing inline tempGitRepo setup with named scenarios:
import { spinUpScenario } from '@gfargo/git-scenarios'
describe('coco commit', () => {
it('writes a message for a feature-PR-ready repo', async () => {
const repo = await spinUpScenario('feature-pr-ready')
try {
// run your command against repo.path …
} finally {
await repo.cleanup()
}
})
})spinUpScenario(name) returns a fully-built TempGitRepo (path + simple-git instance + writeFile/commitAll helpers + cleanup). Replaces the inline "git init + a stack of repo.git.commit(...) calls" pattern for the 90% case where a named scenario fits.
For the rare case where no named scenario fits, createTempGitRepo() exposes the raw primitive — a fresh repo with user identity + a main branch.
| Flag | Behavior |
|---|---|
--path <dir> |
Materialize at <dir> instead of /tmp. Useful when you want to cd into it later. |
--run <cmd> |
After materializing, spawn <cmd> against the scenario dir (cwd = scenario dir). Examples: --run "lazygit", --run "gitui", --run "code -n". |
--run-ui |
Coco-specific shortcut — spawns the source-tree CLI (tsx <coco>/src/index.ts ui) against the scenario dir. External consumers use --run "coco ui" instead. |
--remote <url> |
Add origin pointing at <url> so gh-aware tools detect a remote on launch. |
--ephemeral |
Auto-clean the temp dir when the spawned tool exits. Skip for normal use — without --ephemeral, the dir persists so you can re-inspect after closing the tool. |
-
Package source:
@gfargo/git-scenarios— atoms, scenarios, the CLI binary -
Package on npm:
@gfargo/git-scenarios -
Coco-side wiring:
package.jsondeclares the dep + exposesnpm run scenarioas a shortcut -
Integration test consumer:
src/commands/commands.integration.test.tsusesspinUpScenarioto set up fixtures -
Eval harness consumer:
src/lib/parsers/default/__evals__/scenarioInputs.tsusesfindScenario+createTempGitRepoto build the structural-extract eval corpus
When @gfargo/git-scenarios ships a new version (new scenarios, new atoms, bug fixes), bump the coco dep:
yarn add --dev @gfargo/git-scenarios@^X.Y.ZThen run the integration test to catch any breaking changes:
npx jest src/commands/commands.integration.test.ts- Coco UI — the workstation TUI that scenarios are most often used to drive
- TUI Navigation — the keymap + chord set
- git-scenarios README — full atom catalog, cookbook, and contribution rules