Skip to content

Testing Scenarios

Griffen Fargo edited this page May 18, 2026 · 2 revisions

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:

  1. 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.
  2. Integration tests — use spinUpScenario('feature-pr-ready') in your jest tests to get a fully-built TempGitRepo instead of writing inline git 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.

Quick start

# 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 --ephemeral

The -- separator is required because npm run scenario already accepts subcommands; flags after -- are passed through to the underlying git-scenarios binary.

Available scenarios

Current set: 19 scenarios across 6 kinds. The live list is npm run scenario list — the table below is a quick reference.

Branch shapes

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.

Upstream tracking

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.

Detached / config shapes

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.

Worktree shapes

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.

In-progress operations

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.

History shapes

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.

Stash & submodule shapes

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.

Common dev workflows

"I'm working on the branches view"

# Use the showcase to see every branch-tip-chip variant at once:
npm run scenario create chip-rendering-showcase -- --run-ui

Look 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.

"I'm working on the conflict view"

npm run scenario create mid-merge-conflict -- --run-ui

Lands you in a repo with a real in-progress merge and a real conflict on disk — no mocking needed.

"I'm working on pull/push or ahead-behind indicators"

npm run scenario create branch-diverged -- --run-ui

git status reports "2 ahead, 2 behind." Test that the UI surfaces that and that pull/push affordances do the right thing.

"I want to try something fully ephemeral"

npm run scenario create feature-pr-ready -- --run-ui --ephemeral

Tmp dir gets removed when coco ui exits.

"I want a real remote"

npm run scenario create feature-pr-ready -- --remote git@github.com:org/repo.git --run-ui

Adds 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.

Composing your own scenarios

The scenarios listed above cover common cases. If you need something different, two paths:

Path A — inline in a test

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 assertions

See git-scenarios cookbook for many more recipes.

Path B — contribute a named scenario back

If the shape is reusable, contribute it to @gfargo/git-scenarios:

  1. Add src/scenarios/your-scenario.ts using defineScenario(...) (see existing scenarios for the pattern — every one is a single self-contained file).
  2. Add a .test.ts asserting your scenario's contracts.
  3. Wire it into src/scenarios/index.ts (allScenarios array + the re-export).
  4. Run npm test to verify; submit a PR to gfargo/git-scenarios.

A scenario is roughly 30–60 lines including JSDoc, and defineScenario validates the shape at module-load time so typos surface immediately.

Using scenarios in jest tests

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.

CLI flag reference

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.

Where things live

  • Package source: @gfargo/git-scenarios — atoms, scenarios, the CLI binary
  • Package on npm: @gfargo/git-scenarios
  • Coco-side wiring: package.json declares the dep + exposes npm run scenario as a shortcut
  • Integration test consumer: src/commands/commands.integration.test.ts uses spinUpScenario to set up fixtures
  • Eval harness consumer: src/lib/parsers/default/__evals__/scenarioInputs.ts uses findScenario + createTempGitRepo to build the structural-extract eval corpus

Updating to a new release

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.Z

Then run the integration test to catch any breaking changes:

npx jest src/commands/commands.integration.test.ts

See also

Clone this wiki locally